Skip to content

Commit 9205eb1

Browse files
nullfuncCopilotlionello
authored
enable client side "defang deployment list --active" (#1112)
* init * update naming * clean up * merge * in progress * update vendor hash * re-order listing * add region to output of deployment list * update list active deployments output to match regular list deployements * Update src/pkg/cli/deploymentsList.go Co-authored-by: Copilot <[email protected]> * update from review. allow users to set the number of returned items, make active deployments the default, allow users to see active deployments for a single project * update for limit = 0 to mean unlimited * update limit type to uint32 * Apply suggestions from code review Co-authored-by: Lio李歐 <[email protected]> * review update: use deploymentsList rather than it's own active function. historical list and active lists will contain the same columns. * update go.mod and go.sum * Apply suggestions from code review Co-authored-by: Lio李歐 <[email protected]> * review updates * update vendor hash * update to add a --all flag * review updates regarding flags * review updates * sort on proj, prov, acc, region * fix tests * Update src/cmd/cli/command/commands.go short desc * Revert "Update src/cmd/cli/command/commands.go short desc" This reverts commit a063e75. * Update short description per comments --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Lio李歐 <[email protected]> Co-authored-by: Lionello Lunesu <[email protected]>
1 parent 161a3c9 commit 9205eb1

File tree

4 files changed

+146
-28
lines changed

4 files changed

+146
-28
lines changed

src/cmd/cli/command/commands.go

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -991,15 +991,9 @@ var deploymentsCmd = &cobra.Command{
991991
Aliases: []string{"deployment", "deploys", "deps", "dep"},
992992
Annotations: authNeededAnnotation,
993993
Args: cobra.NoArgs,
994-
Short: "List deployments",
994+
Short: "List active deployments across all projects",
995995
RunE: func(cmd *cobra.Command, args []string) error {
996-
loader := configureLoader(cmd)
997-
projectName, err := loader.LoadProjectName(cmd.Context())
998-
if err != nil {
999-
return err
1000-
}
1001-
1002-
return cli.DeploymentsList(cmd.Context(), projectName, client)
996+
return cli.DeploymentsList(cmd.Context(), defangv1.DeploymentType_DEPLOYMENT_TYPE_ACTIVE, "", *client, 0)
1003997
},
1004998
}
1005999

@@ -1008,16 +1002,15 @@ var deploymentsListCmd = &cobra.Command{
10081002
Aliases: []string{"ls"},
10091003
Annotations: authNeededAnnotation,
10101004
Args: cobra.NoArgs,
1011-
Short: "List deployments",
1012-
Deprecated: "use 'deployments' instead",
1005+
Short: "List deployment history for a project",
10131006
RunE: func(cmd *cobra.Command, args []string) error {
10141007
loader := configureLoader(cmd)
10151008
projectName, err := loader.LoadProjectName(cmd.Context())
10161009
if err != nil {
10171010
return err
10181011
}
10191012

1020-
return cli.DeploymentsList(cmd.Context(), projectName, client)
1013+
return cli.DeploymentsList(cmd.Context(), defangv1.DeploymentType_DEPLOYMENT_TYPE_HISTORY, projectName, *client, 10)
10211014
},
10221015
}
10231016

src/pkg/cli/activeDeployments_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"net/http/httptest"
6+
"strings"
7+
"testing"
8+
9+
"github.com/DefangLabs/defang/src/pkg/term"
10+
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
11+
"github.com/DefangLabs/defang/src/protos/io/defang/v1/defangv1connect"
12+
connect "github.com/bufbuild/connect-go"
13+
)
14+
15+
var emptyDeployments []*defangv1.Deployment
16+
var activeDeployments = []*defangv1.Deployment{
17+
{Project: "projectAA", Provider: defangv1.Provider_AWS, Region: "us-east-1"},
18+
{Project: "projectAB", Provider: defangv1.Provider_AWS, Region: "us-east-2"},
19+
{Project: "projectAC", Provider: defangv1.Provider_AWS, Region: "us-east-3"},
20+
21+
{Project: "projectDA", Provider: defangv1.Provider_DIGITALOCEAN, Region: "us-central-1"},
22+
{Project: "projectDB", Provider: defangv1.Provider_DIGITALOCEAN, Region: "us-central-1"},
23+
24+
{Project: "projectGA", Provider: defangv1.Provider_GCP, Region: "us-central-2"},
25+
{Project: "projectGB", Provider: defangv1.Provider_GCP, Region: "us-central-3"},
26+
27+
{Project: "projectPlayground", Provider: defangv1.Provider_DEFANG, Region: "us-west-1"},
28+
}
29+
30+
type mockActiveDeploymentsHandler struct {
31+
defangv1connect.UnimplementedFabricControllerHandler
32+
testDeploymentsData []*defangv1.Deployment
33+
}
34+
35+
func (g *mockActiveDeploymentsHandler) ListDeployments(ctx context.Context, req *connect.Request[defangv1.ListDeploymentsRequest]) (*connect.Response[defangv1.ListDeploymentsResponse], error) {
36+
return connect.NewResponse(&defangv1.ListDeploymentsResponse{
37+
Deployments: g.testDeploymentsData,
38+
}), nil
39+
}
40+
41+
func TestActiveDeployments(t *testing.T) {
42+
ctx := context.Background()
43+
44+
fabricServer := &mockActiveDeploymentsHandler{}
45+
_, handler := defangv1connect.NewFabricControllerHandler(fabricServer)
46+
server := httptest.NewServer(handler)
47+
t.Cleanup(server.Close)
48+
49+
url := strings.TrimPrefix(server.URL, "http://")
50+
grpcClient, _ := Connect(ctx, url)
51+
52+
t.Run("no active deployments", func(t *testing.T) {
53+
fabricServer.testDeploymentsData = emptyDeployments
54+
stdout, _ := term.SetupTestTerm(t)
55+
56+
err := DeploymentsList(ctx, defangv1.DeploymentType_DEPLOYMENT_TYPE_ACTIVE, "", *grpcClient, 10)
57+
if err != nil {
58+
t.Fatalf("DeploymentsList() error = %v", err)
59+
}
60+
61+
receivedOutput := stdout.String()
62+
expectedOutput := "No deployments found"
63+
64+
if !strings.Contains(receivedOutput, expectedOutput) {
65+
t.Errorf("Expected %s to contain %s", receivedOutput, expectedOutput)
66+
}
67+
})
68+
69+
t.Run("some active deployments", func(t *testing.T) {
70+
fabricServer.testDeploymentsData = activeDeployments
71+
72+
stdout, _ := term.SetupTestTerm(t)
73+
err := DeploymentsList(ctx, defangv1.DeploymentType_DEPLOYMENT_TYPE_ACTIVE, "", *grpcClient, 10)
74+
if err != nil {
75+
t.Fatalf("DeploymentsList() error = %v", err)
76+
}
77+
78+
lines := strings.Split(stdout.String(), "\n")[2:] // Skip first two lines (space and header)
79+
80+
// Verify each provider and project name exists in the output
81+
for _, deployment := range activeDeployments {
82+
match := false
83+
for _, line := range lines {
84+
if strings.Contains(line, strings.ToLower(deployment.Provider.String())) &&
85+
strings.Contains(line, deployment.Project) &&
86+
strings.Contains(line, deployment.Region) {
87+
match = true
88+
break
89+
}
90+
}
91+
if !match {
92+
t.Errorf("Missing expected output for provider %q and project %q", deployment.Provider.String(), deployment.Project)
93+
}
94+
}
95+
})
96+
}

src/pkg/cli/deploymentsList.go

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package cli
22

33
import (
44
"context"
5+
"sort"
6+
"strings"
57
"time"
68

79
"github.com/DefangLabs/defang/src/pkg/cli/client"
@@ -10,37 +12,64 @@ import (
1012
)
1113

1214
type PrintDeployment struct {
13-
Deployment string
14-
Provider string
15-
DeployedAt string
16-
Region string
15+
AccountId string
16+
Deployment string
17+
DeployedAt string
18+
ProjectName string
19+
Provider string
20+
Region string
1721
}
1822

19-
func DeploymentsList(ctx context.Context, projectName string, client client.FabricClient) error {
23+
func DeploymentsList(ctx context.Context, listType defangv1.DeploymentType, projectName string, client client.GrpcClient, limit uint32) error {
2024
response, err := client.ListDeployments(ctx, &defangv1.ListDeploymentsRequest{
21-
Type: defangv1.DeploymentType_DEPLOYMENT_TYPE_HISTORY,
25+
Type: listType,
2226
Project: projectName,
27+
Limit: limit,
2328
})
2429
if err != nil {
2530
return err
2631
}
2732

2833
numDeployments := len(response.Deployments)
2934
if numDeployments == 0 {
30-
_, err := term.Warnf("No deployments found for project %q", projectName)
35+
var err error
36+
if projectName == "" {
37+
_, err = term.Warn("No deployments found")
38+
} else {
39+
_, err = term.Warnf("No deployments found for project %q", projectName)
40+
}
3141
return err
3242
}
3343

3444
// map to Deployment struct
3545
deployments := make([]PrintDeployment, numDeployments)
3646
for i, d := range response.Deployments {
3747
deployments[i] = PrintDeployment{
38-
Deployment: d.Id,
39-
Provider: d.ProviderString, // TODO: use Provider
40-
DeployedAt: d.Timestamp.AsTime().Format(time.RFC3339),
41-
Region: d.Region,
48+
AccountId: d.ProviderAccountId,
49+
DeployedAt: d.Timestamp.AsTime().Format(time.RFC3339),
50+
Deployment: d.Id,
51+
ProjectName: d.Project,
52+
Provider: getProvider(d.Provider, d.ProviderString),
53+
Region: d.Region,
4254
}
4355
}
4456

45-
return term.Table(deployments, []string{"Deployment", "Provider", "DeployedAt"}) // TODO: add region
57+
// sort by project name, provider, account id, and region
58+
sortKeys := make([]string, numDeployments)
59+
for i, d := range deployments {
60+
// TODO: allow user to specify sort order
61+
sortKeys[i] = strings.Join([]string{d.ProjectName, d.Provider, d.AccountId, d.Region}, "|")
62+
}
63+
sort.SliceStable(sortKeys, func(i, j int) bool {
64+
return sortKeys[i] < sortKeys[j]
65+
})
66+
67+
return term.Table(deployments, []string{"ProjectName", "Provider", "AccountId", "Region", "Deployment", "DeployedAt"})
68+
}
69+
70+
func getProvider(provider defangv1.Provider, providerString string) string {
71+
if provider == defangv1.Provider_PROVIDER_UNSPECIFIED {
72+
return providerString
73+
}
74+
return strings.ToLower(provider.String())
4675
}

src/pkg/cli/deploymentsList_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"github.com/DefangLabs/defang/src/pkg/term"
1010
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
1111
"github.com/DefangLabs/defang/src/protos/io/defang/v1/defangv1connect"
12-
"github.com/bufbuild/connect-go"
12+
connect "github.com/bufbuild/connect-go"
1313
"google.golang.org/protobuf/types/known/emptypb"
1414
"google.golang.org/protobuf/types/known/timestamppb"
1515
)
@@ -59,7 +59,7 @@ func TestDeploymentsList(t *testing.T) {
5959

6060
t.Run("no deployments", func(t *testing.T) {
6161
stdout, _ := term.SetupTestTerm(t)
62-
err := DeploymentsList(ctx, "empty", grpcClient)
62+
err := DeploymentsList(ctx, defangv1.DeploymentType_DEPLOYMENT_TYPE_HISTORY, "empty", *grpcClient, 10)
6363
if err != nil {
6464
t.Fatalf("DeploymentsList() error = %v", err)
6565
}
@@ -74,12 +74,12 @@ func TestDeploymentsList(t *testing.T) {
7474

7575
t.Run("some deployments", func(t *testing.T) {
7676
stdout, _ := term.SetupTestTerm(t)
77-
err := DeploymentsList(ctx, "test", grpcClient)
77+
err := DeploymentsList(ctx, defangv1.DeploymentType_DEPLOYMENT_TYPE_HISTORY, "test", *grpcClient, 10)
7878
if err != nil {
7979
t.Fatalf("DeploymentsList() error = %v", err)
8080
}
81-
expectedOutput := "\x1b[1m\nDeployment Provider DeployedAt \x1b[0m" + `
82-
a1b2c3 playground ` + timestamppb.Now().AsTime().Format("2006-01-02T15:04:05Z07:00") + `
81+
expectedOutput := "\x1b[1m\nProjectName Provider AccountId Region Deployment DeployedAt \x1b[0m" + `
82+
test defang 1234567890 us-test-2 a1b2c3 ` + timestamppb.Now().AsTime().Format("2006-01-02T15:04:05Z07:00") + `
8383
`
8484

8585
receivedLines := strings.Split(stdout.String(), "\n")

0 commit comments

Comments
 (0)