Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* build(deps): update `github.com/Scalingo/go-scalingo` to v10
* refactor: replace `errgo` and `pkg/errors` with `github.com/Scalingo/go-utils/errors/v3`
* refactor: autofix by `go fix` and golangci-lint
* feat: add JSON output for the `apps` command

## 1.43.3

Expand Down
44 changes: 20 additions & 24 deletions apps/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@ package apps

import (
"context"
"fmt"
"os"

"github.com/olekukonko/tablewriter"

"github.com/Scalingo/cli/config"
"github.com/Scalingo/cli/io"
"github.com/Scalingo/cli/utils"
"github.com/Scalingo/go-scalingo/v10"
"github.com/Scalingo/go-utils/errors/v3"
)

func List(ctx context.Context, projectSlug string) error {
type ListRenderer interface {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I don't like is that it would require an interface per command. I don't really like that. I would love to have a single interface whatever the command. And the command only calls Render(ctx). Then we would need a way to inject the data (list of apps or anything else depending on the command) in the renderer.

Render(ctx context.Context, apps []*scalingo.App) error
}

func List(ctx context.Context, renderer ListRenderer, projectSlug string) error {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I like with this pattern is that the List command does not know anything about the format. It just need to calls Render.

c, err := config.ScalingoClient(ctx)
if err != nil {
return errors.Wrap(ctx, err, "get Scalingo client")
Expand All @@ -24,30 +23,27 @@ func List(ctx context.Context, projectSlug string) error {
return errors.Wrap(ctx, err, "list apps")
}

if len(apps) == 0 {
fmt.Println(io.Indent("\nYou haven't created any app yet, create your first application using:\n→ scalingo create <app_name>\n", 2))
return nil
filteredApps := filterAppsByProject(apps, projectSlug)

err = renderer.Render(ctx, filteredApps)
if err != nil {
return errors.Wrap(ctx, err, "render apps list")
}

t := tablewriter.NewWriter(os.Stdout)
t.Header([]string{"Name", "Role", "Status", "Project"})
return nil
}

currentUser, err := config.C.CurrentUser(ctx)
if err != nil {
return errors.Wrap(ctx, err, "fail to get current user")
func filterAppsByProject(apps []*scalingo.App, projectSlug string) []*scalingo.App {
if projectSlug == "" {
return apps
}

filteredApps := make([]*scalingo.App, 0, len(apps))
for _, app := range apps {
// If a filter was set but the app is not in the project, skip to the next one.
if projectSlug != "" && projectSlug != app.ProjectSlug() {
continue
if app.ProjectSlug() == projectSlug {
filteredApps = append(filteredApps, app)
}

role := utils.AppRole(currentUser, app)

_ = t.Append([]string{app.Name, string(role), string(app.Status), app.ProjectSlug()})
}
_ = t.Render()

return nil
return filteredApps
}
29 changes: 26 additions & 3 deletions cmd/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import (

"github.com/Scalingo/cli/apps"
"github.com/Scalingo/cli/cmd/autocomplete"
"github.com/Scalingo/cli/config"
"github.com/Scalingo/cli/detect"
"github.com/Scalingo/cli/internal/boundaries/out/renderer"
rendererjson "github.com/Scalingo/cli/internal/boundaries/out/renderer/json"
renderertable "github.com/Scalingo/cli/internal/boundaries/out/renderer/table"
"github.com/Scalingo/cli/io"
"github.com/Scalingo/cli/utils"
"github.com/Scalingo/go-utils/errors/v3"
Expand All @@ -19,17 +23,36 @@ var (
Name: "apps",
Category: "Global",
Description: "List your apps and give some details about them",
Flags: []cli.Flag{&cli.StringFlag{Name: "project", Usage: "Filter apps by project. The filter uses the format <ownerUsername>/<projectName>"}},
Usage: "List your apps",
Flags: []cli.Flag{
&cli.StringFlag{Name: "project", Usage: "Filter apps by project. The filter uses the format <ownerUsername>/<projectName>"},
},
Usage: "List your apps",
Action: func(ctx context.Context, c *cli.Command) error {
projectSlug := c.String("project")
format := renderer.Format(c.String("format"))

if projectSlug != "" {
projectSlugSplit := strings.Split(projectSlug, "/")
if len(projectSlugSplit) != 2 || (len(projectSlugSplit) == 2 && (projectSlugSplit[0] == "" || projectSlugSplit[1] == "")) {
errorQuitWithHelpMessage(ctx, errors.New(ctx, "project filter doesn't respect the expected format"), c, "apps")
}
}
err := apps.List(ctx, projectSlug)

var appsListRenderer apps.ListRenderer
switch format {
case renderer.FormatTable:
currentUser, err := config.C.CurrentUser(ctx)
if err != nil {
errorQuit(ctx, errors.Wrap(ctx, err, "get current user"))
}
appsListRenderer = renderertable.NewAppsList(currentUser)
case renderer.FormatJSON:
appsListRenderer = rendererjson.NewAppsList()
default:
errorQuitWithHelpMessage(ctx, errors.Newf(ctx, "invalid format '%v'", format), c, "apps")
}

err := apps.List(ctx, appsListRenderer, projectSlug)
if err != nil {
errorQuit(ctx, err)
}
Expand Down
8 changes: 8 additions & 0 deletions internal/boundaries/out/renderer/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package renderer

type Format string

const (
FormatJSON Format = "json"
FormatTable Format = "table"
)
30 changes: 30 additions & 0 deletions internal/boundaries/out/renderer/json/apps_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package json

import (
"context"
"encoding/json"
"os"

"github.com/Scalingo/cli/apps"
"github.com/Scalingo/go-scalingo/v10"
"github.com/Scalingo/go-utils/errors/v3"
)

type appsListRenderer struct {
}

type appsListResponse struct {
Apps []*scalingo.App `json:"apps"`
}

func NewAppsList() apps.ListRenderer {
return appsListRenderer{}
}

func (r appsListRenderer) Render(ctx context.Context, apps []*scalingo.App) error {
err := json.NewEncoder(os.Stdout).Encode(appsListResponse{Apps: apps})
if err != nil {
return errors.Wrap(ctx, err, "encode apps list to JSON")
}
return nil
}
45 changes: 45 additions & 0 deletions internal/boundaries/out/renderer/table/apps_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package table

import (
"context"
"fmt"
"os"

"github.com/olekukonko/tablewriter"

"github.com/Scalingo/cli/apps"
"github.com/Scalingo/cli/io"
"github.com/Scalingo/cli/utils"
"github.com/Scalingo/go-scalingo/v10"
"github.com/Scalingo/go-utils/errors/v3"
)

type appsListRenderer struct {
currentUser *scalingo.User
}

func NewAppsList(currentUser *scalingo.User) apps.ListRenderer {
return appsListRenderer{
currentUser: currentUser,
}
}

func (r appsListRenderer) Render(ctx context.Context, apps []*scalingo.App) error {
if len(apps) == 0 {
fmt.Println(io.Indent("\nYou haven't created any app yet, create your first application using:\n→ scalingo create <app_name>\n", 2))
return nil
}

t := tablewriter.NewWriter(os.Stdout)
t.Header([]string{"Name", "Role", "Status", "Project"})

for _, app := range apps {
role := utils.AppRole(r.currentUser, app)
err := t.Append([]string{app.Name, string(role), string(app.Status), app.ProjectSlug()})
if err != nil {
return errors.Wrap(ctx, err, "append app to table")
}
}

return t.Render()
}
2 changes: 2 additions & 0 deletions scalingo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/Scalingo/cli/cmd"
"github.com/Scalingo/cli/cmd/autocomplete"
"github.com/Scalingo/cli/config"
"github.com/Scalingo/cli/internal/boundaries/out/renderer"
"github.com/Scalingo/cli/signals"
"github.com/Scalingo/cli/update"
"github.com/Scalingo/go-scalingo/v10/debug"
Expand Down Expand Up @@ -88,6 +89,7 @@ func main() {
app.Flags = []cli.Flag{
&cli.StringFlag{Name: "addon", Value: "<addon_id>", Usage: "ID of the current addon", Sources: cli.EnvVars("SCALINGO_ADDON")},
&cli.StringFlag{Name: "app", Aliases: []string{"a"}, Value: "<name>", Usage: "Name of the app", Sources: cli.EnvVars("SCALINGO_APP")},
&cli.StringFlag{Name: "format", Value: string(renderer.FormatTable), Usage: "[" + string(renderer.FormatJSON) + "|" + string(renderer.FormatTable) + "]"},
&cli.StringFlag{Name: "remote", Aliases: []string{"r"}, Value: "scalingo", Usage: "Name of the remote"},
&cli.StringFlag{Name: "region", Value: "", Usage: "Name of the region to use"},
}
Expand Down