From 3394ce77d4de1477d8aff5b1317b9db1fe4bc414 Mon Sep 17 00:00:00 2001 From: penghaoh Date: Mon, 27 Apr 2020 19:18:10 -0700 Subject: [PATCH 1/3] feat(cli): add app show support for backend app --- internal/pkg/cli/app_show.go | 47 ++-- internal/pkg/cli/app_show_test.go | 25 +- internal/pkg/cli/interfaces.go | 4 +- internal/pkg/cli/mocks/mock_interfaces.go | 30 +-- internal/pkg/describe/backend_app.go | 215 +++++++++++++++ internal/pkg/describe/backend_app_test.go | 248 ++++++++++++++++++ internal/pkg/describe/lb_web_app.go | 93 +++++-- internal/pkg/describe/lb_web_app_test.go | 27 +- .../pkg/describe/mocks/mock_lb_web_app.go | 52 ++++ 9 files changed, 672 insertions(+), 69 deletions(-) create mode 100644 internal/pkg/describe/backend_app.go create mode 100644 internal/pkg/describe/backend_app_test.go diff --git a/internal/pkg/cli/app_show.go b/internal/pkg/cli/app_show.go index 11740fb4629..c496a48e3a9 100644 --- a/internal/pkg/cli/app_show.go +++ b/internal/pkg/cli/app_show.go @@ -9,6 +9,7 @@ import ( "io" "github.com/aws/amazon-ecs-cli-v2/internal/pkg/describe" + "github.com/aws/amazon-ecs-cli-v2/internal/pkg/manifest" "github.com/aws/amazon-ecs-cli-v2/internal/pkg/store" "github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/color" "github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/log" @@ -35,9 +36,9 @@ type showAppOpts struct { w io.Writer storeSvc storeReader - describer webAppDescriber + describer appDescriber ws wsAppReader - initDescriber func(*showAppOpts, bool) error // Overriden in tests. + initDescriber func(bool) error // Overriden in tests. } func newShowAppOpts(vars showAppVars) (*showAppOpts, error) { @@ -50,26 +51,42 @@ func newShowAppOpts(vars showAppVars) (*showAppOpts, error) { return nil, err } - return &showAppOpts{ + opts := &showAppOpts{ showAppVars: vars, storeSvc: ssmStore, ws: ws, w: log.OutputWriter, - initDescriber: func(o *showAppOpts, enableResources bool) error { - var d *describe.WebAppDescriber - var err error + } + opts.initDescriber = func(enableResources bool) error { + var d appDescriber + app, err := opts.storeSvc.GetApplication(opts.ProjectName(), opts.appName) + if err != nil { + return err + } + switch app.Type { + case manifest.LoadBalancedWebApplication: if enableResources { - d, err = describe.NewWebAppDescriberWithResources(o.ProjectName(), o.appName) + d, err = describe.NewWebAppDescriberWithResources(opts.ProjectName(), opts.appName) } else { - d, err = describe.NewWebAppDescriber(o.ProjectName(), o.appName) + d, err = describe.NewWebAppDescriber(opts.ProjectName(), opts.appName) } - if err != nil { - return fmt.Errorf("creating describer for application %s in project %s: %w", o.appName, o.ProjectName(), err) + case manifest.BackendApplication: + if enableResources { + d, err = describe.NewBackendAppDescriberWithResources(opts.ProjectName(), opts.appName) + } else { + d, err = describe.NewBackendAppDescriber(opts.ProjectName(), opts.appName) } - o.describer = d - return nil - }, - }, nil + default: + return fmt.Errorf("invalid application type %s", app.Type) + } + + if err != nil { + return fmt.Errorf("creating describer for application %s in project %s: %w", opts.appName, opts.ProjectName(), err) + } + opts.describer = d + return nil + } + return opts, nil } // Validate returns an error if the values provided by the user are invalid. @@ -102,7 +119,7 @@ func (o *showAppOpts) Execute() error { // If there are no local applications in the workspace, we exit without error. return nil } - err := o.initDescriber(o, o.shouldOutputResources) + err := o.initDescriber(o.shouldOutputResources) if err != nil { return err } diff --git a/internal/pkg/cli/app_show_test.go b/internal/pkg/cli/app_show_test.go index c36f8fde0f4..ac0e3e97c44 100644 --- a/internal/pkg/cli/app_show_test.go +++ b/internal/pkg/cli/app_show_test.go @@ -19,7 +19,7 @@ import ( type showAppMocks struct { storeSvc *climocks.MockstoreReader prompt *climocks.Mockprompter - describer *climocks.MockwebAppDescriber + describer *climocks.MockappDescriber ws *climocks.MockwsAppReader } @@ -372,7 +372,7 @@ func TestAppShow_Execute(t *testing.T) { projectName := "my-project" webApp := describe.WebAppDesc{ AppName: "my-app", - Configurations: []*describe.WebAppConfig{ + Configurations: []*describe.AppConfig{ { CPU: "256", Environment: "test", @@ -411,6 +411,12 @@ func TestAppShow_Execute(t *testing.T) { URL: "http://my-pr-Publi.us-west-2.elb.amazonaws.com/backend", }, }, + ServiceDiscovery: []*describe.ServiceDiscovery{ + { + Environment: []string{"test", "prod"}, + Namespace: "http://my-app.my-project.local:5000", + }, + }, Resources: map[string][]*describe.CfnResource{ "test": []*describe.CfnResource{ { @@ -452,7 +458,7 @@ func TestAppShow_Execute(t *testing.T) { ) }, - wantedContent: "{\"appName\":\"my-app\",\"type\":\"\",\"project\":\"my-project\",\"configurations\":[{\"environment\":\"test\",\"port\":\"80\",\"tasks\":\"1\",\"cpu\":\"256\",\"memory\":\"512\"},{\"environment\":\"prod\",\"port\":\"5000\",\"tasks\":\"3\",\"cpu\":\"512\",\"memory\":\"1024\"}],\"routes\":[{\"environment\":\"test\",\"url\":\"http://my-pr-Publi.us-west-2.elb.amazonaws.com/frontend\"},{\"environment\":\"prod\",\"url\":\"http://my-pr-Publi.us-west-2.elb.amazonaws.com/backend\"}],\"variables\":[{\"environment\":\"prod\",\"name\":\"ECS_CLI_ENVIRONMENT_NAME\",\"value\":\"prod\"},{\"environment\":\"test\",\"name\":\"ECS_CLI_ENVIRONMENT_NAME\",\"value\":\"test\"}],\"resources\":{\"prod\":[{\"type\":\"AWS::EC2::SecurityGroupIngress\",\"physicalID\":\"ContainerSecurityGroupIngressFromPublicALB\"}],\"test\":[{\"type\":\"AWS::EC2::SecurityGroup\",\"physicalID\":\"sg-0758ed6b233743530\"}]}}\n", + wantedContent: "{\"appName\":\"my-app\",\"type\":\"\",\"project\":\"my-project\",\"configurations\":[{\"environment\":\"test\",\"port\":\"80\",\"tasks\":\"1\",\"cpu\":\"256\",\"memory\":\"512\"},{\"environment\":\"prod\",\"port\":\"5000\",\"tasks\":\"3\",\"cpu\":\"512\",\"memory\":\"1024\"}],\"routes\":[{\"environment\":\"test\",\"url\":\"http://my-pr-Publi.us-west-2.elb.amazonaws.com/frontend\"},{\"environment\":\"prod\",\"url\":\"http://my-pr-Publi.us-west-2.elb.amazonaws.com/backend\"}],\"serviceDiscovery\":[{\"environment\":[\"test\",\"prod\"],\"namespace\":\"http://my-app.my-project.local:5000\"}],\"variables\":[{\"environment\":\"prod\",\"name\":\"ECS_CLI_ENVIRONMENT_NAME\",\"value\":\"prod\"},{\"environment\":\"test\",\"name\":\"ECS_CLI_ENVIRONMENT_NAME\",\"value\":\"test\"}],\"resources\":{\"prod\":[{\"type\":\"AWS::EC2::SecurityGroupIngress\",\"physicalID\":\"ContainerSecurityGroupIngressFromPublicALB\"}],\"test\":[{\"type\":\"AWS::EC2::SecurityGroup\",\"physicalID\":\"sg-0758ed6b233743530\"}]}}\n", }, "prompt for all input for human output": { inputApp: "my-app", @@ -483,6 +489,11 @@ Routes test http://my-pr-Publi.us-west-2.elb.amazonaws.com/frontend prod http://my-pr-Publi.us-west-2.elb.amazonaws.com/backend +Service Discovery + + Environment Namespace + test, prod http://my-app.my-project.local:5000 + Variables Name Environment Value @@ -517,10 +528,10 @@ Resources defer ctrl.Finish() b := &bytes.Buffer{} - mockWebAppDescriber := climocks.NewMockwebAppDescriber(ctrl) + mockAppDescriber := climocks.NewMockappDescriber(ctrl) mocks := showAppMocks{ - describer: mockWebAppDescriber, + describer: mockAppDescriber, } tc.setupMocks(mocks) @@ -534,8 +545,8 @@ Resources projectName: projectName, }, }, - describer: mockWebAppDescriber, - initDescriber: func(*showAppOpts, bool) error { return nil }, + describer: mockAppDescriber, + initDescriber: func(bool) error { return nil }, w: b, } diff --git a/internal/pkg/cli/interfaces.go b/internal/pkg/cli/interfaces.go index 4f323359af1..ba8772f60e5 100644 --- a/internal/pkg/cli/interfaces.go +++ b/internal/pkg/cli/interfaces.go @@ -91,8 +91,8 @@ type sessionProvider interface { sessionFromRoleProvider } -type webAppDescriber interface { - Describe() (*describe.WebAppDesc, error) +type appDescriber interface { + Describe() (describe.HumanJSONStringer, error) } type storeReader interface { diff --git a/internal/pkg/cli/mocks/mock_interfaces.go b/internal/pkg/cli/mocks/mock_interfaces.go index 4d96024623d..b205b99582a 100644 --- a/internal/pkg/cli/mocks/mock_interfaces.go +++ b/internal/pkg/cli/mocks/mock_interfaces.go @@ -825,42 +825,42 @@ func (mr *MocksessionProviderMockRecorder) FromRole(roleARN, region interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FromRole", reflect.TypeOf((*MocksessionProvider)(nil).FromRole), roleARN, region) } -// MockwebAppDescriber is a mock of webAppDescriber interface -type MockwebAppDescriber struct { +// MockappDescriber is a mock of appDescriber interface +type MockappDescriber struct { ctrl *gomock.Controller - recorder *MockwebAppDescriberMockRecorder + recorder *MockappDescriberMockRecorder } -// MockwebAppDescriberMockRecorder is the mock recorder for MockwebAppDescriber -type MockwebAppDescriberMockRecorder struct { - mock *MockwebAppDescriber +// MockappDescriberMockRecorder is the mock recorder for MockappDescriber +type MockappDescriberMockRecorder struct { + mock *MockappDescriber } -// NewMockwebAppDescriber creates a new mock instance -func NewMockwebAppDescriber(ctrl *gomock.Controller) *MockwebAppDescriber { - mock := &MockwebAppDescriber{ctrl: ctrl} - mock.recorder = &MockwebAppDescriberMockRecorder{mock} +// NewMockappDescriber creates a new mock instance +func NewMockappDescriber(ctrl *gomock.Controller) *MockappDescriber { + mock := &MockappDescriber{ctrl: ctrl} + mock.recorder = &MockappDescriberMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use -func (m *MockwebAppDescriber) EXPECT() *MockwebAppDescriberMockRecorder { +func (m *MockappDescriber) EXPECT() *MockappDescriberMockRecorder { return m.recorder } // Describe mocks base method -func (m *MockwebAppDescriber) Describe() (*describe.WebAppDesc, error) { +func (m *MockappDescriber) Describe() (describe.HumanJSONStringer, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Describe") - ret0, _ := ret[0].(*describe.WebAppDesc) + ret0, _ := ret[0].(describe.HumanJSONStringer) ret1, _ := ret[1].(error) return ret0, ret1 } // Describe indicates an expected call of Describe -func (mr *MockwebAppDescriberMockRecorder) Describe() *gomock.Call { +func (mr *MockappDescriberMockRecorder) Describe() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Describe", reflect.TypeOf((*MockwebAppDescriber)(nil).Describe)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Describe", reflect.TypeOf((*MockappDescriber)(nil).Describe)) } // MockstoreReader is a mock of storeReader interface diff --git a/internal/pkg/describe/backend_app.go b/internal/pkg/describe/backend_app.go new file mode 100644 index 00000000000..9ccf986d492 --- /dev/null +++ b/internal/pkg/describe/backend_app.go @@ -0,0 +1,215 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package describe + +import ( + "bytes" + "encoding/json" + "fmt" + "sort" + "strings" + "text/tabwriter" + + "github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer" + "github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy/cloudformation/stack" + "github.com/aws/amazon-ecs-cli-v2/internal/pkg/store" + "github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/color" +) + +// BackendAppDescriber retrieves information about a backend application. +type BackendAppDescriber struct { + app *archer.Application + enableResources bool + + store storeSvc + appDescriber appDescriber + initAppDescriber func(string) error +} + +// NewBackendAppDescriber instantiates a backend application describer. +func NewBackendAppDescriber(project, app string) (*BackendAppDescriber, error) { + svc, err := store.New() + if err != nil { + return nil, fmt.Errorf("connect to store: %w", err) + } + meta, err := svc.GetApplication(project, app) + if err != nil { + return nil, err + } + opts := &BackendAppDescriber{ + app: meta, + enableResources: false, + store: svc, + } + opts.initAppDescriber = func(env string) error { + d, err := NewAppDescriber(project, env, app) + if err != nil { + return err + } + opts.appDescriber = d + return nil + } + return opts, nil +} + +// NewBackendAppDescriberWithResources instantiates a backend application with stack resources. +func NewBackendAppDescriberWithResources(project, app string) (*BackendAppDescriber, error) { + d, err := NewBackendAppDescriber(project, app) + if err != nil { + return nil, err + } + d.enableResources = true + return d, nil +} + +// Describe returns info of a backend application. +func (d *BackendAppDescriber) Describe() (HumanJSONStringer, error) { + environments, err := d.store.ListEnvironments(d.app.Project) + if err != nil { + return nil, fmt.Errorf("list environments: %w", err) + } + + var configs []*AppConfig + var services []*ServiceDiscovery + var envVars []*EnvVars + for _, env := range environments { + err := d.initAppDescriber(env.Name) + if err != nil { + return nil, err + } + appParams, err := d.appDescriber.Params() + if err == nil { + services = appendServiceDiscovery(services, serviceDiscovery{ + AppName: d.app.Name, + Port: appParams[stack.LBWebAppContainerPortParamKey], + ProjectName: d.app.Project, + }, env.Name) + configs = append(configs, &AppConfig{ + Environment: env.Name, + Port: appParams[stack.LBWebAppContainerPortParamKey], + Tasks: appParams[stack.AppTaskCountParamKey], + CPU: appParams[stack.AppTaskCPUParamKey], + Memory: appParams[stack.AppTaskMemoryParamKey], + }) + backendAppEnvVars, err := d.appDescriber.EnvVars() + if err != nil { + return nil, fmt.Errorf("retrieve environment variables: %w", err) + } + envVars = append(envVars, flattenEnvVars(env.Name, backendAppEnvVars)...) + continue + } + if !IsStackNotExistsErr(err) { + return nil, fmt.Errorf("retrieve application deployment configuration: %w", err) + } + } + sort.SliceStable(envVars, func(i, j int) bool { return envVars[i].Environment < envVars[j].Environment }) + sort.SliceStable(envVars, func(i, j int) bool { return envVars[i].Name < envVars[j].Name }) + + resources := make(map[string][]*CfnResource) + if d.enableResources { + for _, env := range environments { + stackResources, err := d.appDescriber.AppStackResources() + if err == nil { + resources[env.Name] = flattenResources(stackResources) + continue + } + if !IsStackNotExistsErr(err) { + return nil, fmt.Errorf("retrieve application resources: %w", err) + } + } + } + + return &BackendAppDesc{ + AppName: d.app.Name, + Type: d.app.Type, + Project: d.app.Project, + Configurations: configs, + ServiceDiscovery: services, + Variables: envVars, + Resources: resources, + }, nil +} + +// BackendAppDesc contains serialized parameters for a backend application. +type BackendAppDesc struct { + AppName string `json:"appName"` + Type string `json:"type"` + Project string `json:"project"` + Configurations []*AppConfig `json:"configurations"` + ServiceDiscovery []*ServiceDiscovery `json:"serviceDiscovery"` + Variables []*EnvVars `json:"variables"` + Resources map[string][]*CfnResource `json:"resources,omitempty"` +} + +// JSONString returns the stringified BackendApp struct with json format. +func (w *BackendAppDesc) JSONString() (string, error) { + b, err := json.Marshal(w) + if err != nil { + return "", fmt.Errorf("marshal applications: %w", err) + } + return fmt.Sprintf("%s\n", b), nil +} + +// HumanString returns the stringified BackendApp struct with human readable format. +func (w *BackendAppDesc) HumanString() string { + var b bytes.Buffer + writer := tabwriter.NewWriter(&b, minCellWidth, tabWidth, cellPaddingWidth, paddingChar, noAdditionalFormatting) + fmt.Fprintf(writer, color.Bold.Sprint("About\n\n")) + writer.Flush() + fmt.Fprintf(writer, " %s\t%s\n", "Project", w.Project) + fmt.Fprintf(writer, " %s\t%s\n", "Name", w.AppName) + fmt.Fprintf(writer, " %s\t%s\n", "Type", w.Type) + fmt.Fprintf(writer, color.Bold.Sprint("\nConfigurations\n\n")) + writer.Flush() + fmt.Fprintf(writer, " %s\t%s\t%s\t%s\t%s\n", "Environment", "Tasks", "CPU (vCPU)", "Memory (MiB)", "Port") + for _, config := range w.Configurations { + fmt.Fprintf(writer, " %s\t%s\t%s\t%s\t%s\n", config.Environment, config.Tasks, cpuToString(config.CPU), config.Memory, config.Port) + } + fmt.Fprintf(writer, color.Bold.Sprint("\nService Discovery\n\n")) + writer.Flush() + fmt.Fprintf(writer, " %s\t%s\n", "Environment", "Namespace") + for _, sd := range w.ServiceDiscovery { + fmt.Fprintf(writer, " %s\t%s\n", strings.Join(sd.Environment, ", "), sd.Namespace) + } + fmt.Fprintf(writer, color.Bold.Sprint("\nVariables\n\n")) + writer.Flush() + fmt.Fprintf(writer, " %s\t%s\t%s\n", "Name", "Environment", "Value") + var prevName string + var prevValue string + for _, variable := range w.Variables { + // Instead of re-writing the same variable value, we replace it with "-" to reduce text. + if variable.Name != prevName { + if variable.Value != prevValue { + fmt.Fprintf(writer, " %s\t%s\t%s\n", variable.Name, variable.Environment, variable.Value) + } else { + fmt.Fprintf(writer, " %s\t%s\t-\n", variable.Name, variable.Environment) + } + } else { + if variable.Value != prevValue { + fmt.Fprintf(writer, " -\t%s\t%s\n", variable.Environment, variable.Value) + } else { + fmt.Fprintf(writer, " -\t%s\t-\n", variable.Environment) + } + } + prevName = variable.Name + prevValue = variable.Value + } + if len(w.Resources) != 0 { + fmt.Fprintf(writer, color.Bold.Sprint("\nResources\n")) + writer.Flush() + + // Go maps don't have a guaranteed order. + // Show the resources by the order of environments displayed under Routes for a consistent view. + for _, config := range w.Configurations { + env := config.Environment + resources := w.Resources[env] + fmt.Fprintf(writer, "\n %s\n", env) + for _, resource := range resources { + fmt.Fprintf(writer, " %s\t%s\n", resource.Type, resource.PhysicalID) + } + } + } + writer.Flush() + return b.String() +} diff --git a/internal/pkg/describe/backend_app_test.go b/internal/pkg/describe/backend_app_test.go new file mode 100644 index 00000000000..f1581e3173b --- /dev/null +++ b/internal/pkg/describe/backend_app_test.go @@ -0,0 +1,248 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package describe + +import ( + "errors" + "fmt" + "testing" + + "github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer" + "github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy/cloudformation/stack" + "github.com/aws/amazon-ecs-cli-v2/internal/pkg/describe/mocks" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" +) + +type backendAppDescriberMocks struct { + storeSvc *mocks.MockstoreSvc + appDescriber *mocks.MockappDescriber +} + +func TestBackendAppDescriber_Describe(t *testing.T) { + const ( + testProject = "phonetool" + testEnv = "test" + testApp = "jobs" + testAppPath = "*" + prodEnv = "prod" + prodAppPath = "*" + ) + mockErr := errors.New("some error") + mockNotExistErr := awserr.New("ValidationError", "Stack with id mockID does not exist", nil) + testEnvironment := archer.Environment{ + Project: testProject, + Name: testEnv, + } + prodEnvironment := archer.Environment{ + Project: testProject, + Name: prodEnv, + } + testCases := map[string]struct { + shouldOutputResources bool + + setupMocks func(mocks backendAppDescriberMocks) + + wantedBackendApp *BackendAppDesc + wantedError error + }{ + "return error if fail to list environment": { + setupMocks: func(m backendAppDescriberMocks) { + gomock.InOrder( + m.storeSvc.EXPECT().ListEnvironments(testProject).Return(nil, mockErr), + ) + }, + wantedError: fmt.Errorf("list environments: some error"), + }, + "return error if fail to retrieve application deployment configuration": { + setupMocks: func(m backendAppDescriberMocks) { + gomock.InOrder( + m.storeSvc.EXPECT().ListEnvironments(testProject).Return([]*archer.Environment{ + &testEnvironment, + }, nil), + m.appDescriber.EXPECT().Params().Return(nil, mockErr), + ) + }, + wantedError: fmt.Errorf("retrieve application deployment configuration: some error"), + }, + "return error if fail to retrieve environment variables": { + setupMocks: func(m backendAppDescriberMocks) { + gomock.InOrder( + m.storeSvc.EXPECT().ListEnvironments(testProject).Return([]*archer.Environment{ + &testEnvironment, + }, nil), + m.appDescriber.EXPECT().Params().Return(map[string]string{ + stack.LBWebAppContainerPortParamKey: "80", + stack.AppTaskCountParamKey: "1", + stack.AppTaskCPUParamKey: "256", + stack.AppTaskMemoryParamKey: "512", + }, nil), + m.appDescriber.EXPECT().EnvVars().Return(nil, mockErr), + ) + }, + wantedError: fmt.Errorf("retrieve environment variables: some error"), + }, + "skip if not deployed": { + shouldOutputResources: true, + setupMocks: func(m backendAppDescriberMocks) { + gomock.InOrder( + m.storeSvc.EXPECT().ListEnvironments(testProject).Return([]*archer.Environment{ + &testEnvironment, + }, nil), + m.appDescriber.EXPECT().Params().Return(nil, mockNotExistErr), + m.appDescriber.EXPECT().AppStackResources().Return(nil, mockNotExistErr), + ) + }, + wantedBackendApp: &BackendAppDesc{ + AppName: testApp, + Type: "", + Project: testProject, + Configurations: []*AppConfig(nil), + ServiceDiscovery: []*ServiceDiscovery(nil), + Variables: []*EnvVars(nil), + Resources: make(map[string][]*CfnResource), + }, + }, + "success": { + shouldOutputResources: true, + setupMocks: func(m backendAppDescriberMocks) { + gomock.InOrder( + m.storeSvc.EXPECT().ListEnvironments(testProject).Return([]*archer.Environment{ + &testEnvironment, + &prodEnvironment, + }, nil), + + m.appDescriber.EXPECT().Params().Return(map[string]string{ + stack.LBWebAppContainerPortParamKey: "5000", + stack.AppTaskCountParamKey: "1", + stack.AppTaskCPUParamKey: "256", + stack.AppTaskMemoryParamKey: "512", + }, nil), + m.appDescriber.EXPECT().EnvVars().Return( + map[string]string{ + "ECS_CLI_ENVIRONMENT_NAME": testEnv, + }, nil), + + m.appDescriber.EXPECT().Params().Return(map[string]string{ + stack.LBWebAppContainerPortParamKey: "5000", + stack.AppTaskCountParamKey: "2", + stack.AppTaskCPUParamKey: "512", + stack.AppTaskMemoryParamKey: "1024", + }, nil), + m.appDescriber.EXPECT().EnvVars().Return( + map[string]string{ + "ECS_CLI_ENVIRONMENT_NAME": prodEnv, + }, nil), + + m.appDescriber.EXPECT().AppStackResources().Return([]*cloudformation.StackResource{ + { + ResourceType: aws.String("AWS::EC2::SecurityGroupIngress"), + PhysicalResourceId: aws.String("ContainerSecurityGroupIngressFromPublicALB"), + }, + }, nil), + m.appDescriber.EXPECT().AppStackResources().Return([]*cloudformation.StackResource{ + { + ResourceType: aws.String("AWS::EC2::SecurityGroup"), + PhysicalResourceId: aws.String("sg-0758ed6b233743530"), + }, + }, nil), + ) + }, + wantedBackendApp: &BackendAppDesc{ + AppName: testApp, + Type: "", + Project: testProject, + Configurations: []*AppConfig{ + { + CPU: "256", + Environment: "test", + Memory: "512", + Port: "5000", + Tasks: "1", + }, + { + CPU: "512", + Environment: "prod", + Memory: "1024", + Port: "5000", + Tasks: "2", + }, + }, + ServiceDiscovery: []*ServiceDiscovery{ + { + Environment: []string{"test", "prod"}, + Namespace: "http://jobs.phonetool.local:5000", + }, + }, + Variables: []*EnvVars{ + { + Environment: "prod", + Name: "ECS_CLI_ENVIRONMENT_NAME", + Value: "prod", + }, + { + Environment: "test", + Name: "ECS_CLI_ENVIRONMENT_NAME", + Value: "test", + }, + }, + Resources: map[string][]*CfnResource{ + "test": []*CfnResource{ + { + Type: "AWS::EC2::SecurityGroupIngress", + PhysicalID: "ContainerSecurityGroupIngressFromPublicALB", + }, + }, + "prod": []*CfnResource{ + { + Type: "AWS::EC2::SecurityGroup", + PhysicalID: "sg-0758ed6b233743530", + }, + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := mocks.NewMockstoreSvc(ctrl) + mockAppDescriber := mocks.NewMockappDescriber(ctrl) + mocks := backendAppDescriberMocks{ + storeSvc: mockStore, + appDescriber: mockAppDescriber, + } + + tc.setupMocks(mocks) + + d := &BackendAppDescriber{ + app: &archer.Application{ + Project: testProject, + Name: testApp, + }, + enableResources: tc.shouldOutputResources, + store: mockStore, + appDescriber: mockAppDescriber, + initAppDescriber: func(string) error { return nil }, + } + + // WHEN + backendapp, err := d.Describe() + + // THEN + if tc.wantedError != nil { + require.EqualError(t, err, tc.wantedError.Error()) + } else { + require.Nil(t, err) + require.Equal(t, tc.wantedBackendApp, backendapp, "expected output content match") + } + }) + } +} diff --git a/internal/pkg/describe/lb_web_app.go b/internal/pkg/describe/lb_web_app.go index e5c026416b4..458be821d81 100644 --- a/internal/pkg/describe/lb_web_app.go +++ b/internal/pkg/describe/lb_web_app.go @@ -55,6 +55,16 @@ func (uri *WebAppURI) String() string { } } +type serviceDiscovery struct { + AppName string + ProjectName string + Port string +} + +func (s *serviceDiscovery) String() string { + return fmt.Sprintf("http://%s.%s.local:%s", s.AppName, s.ProjectName, s.Port) +} + type storeSvc interface { GetEnvironment(projectName string, environmentName string) (*archer.Environment, error) ListEnvironments(projectName string) ([]*archer.Environment, error) @@ -68,6 +78,12 @@ type appDescriber interface { AppStackResources() ([]*cloudformation.StackResource, error) } +// HumanJSONStringer contains methods that stringify app info for output. +type HumanJSONStringer interface { + HumanString() string + JSONString() (string, error) +} + // WebAppDescriber retrieves information about a load balanced web application. type WebAppDescriber struct { app *archer.Application @@ -78,7 +94,7 @@ type WebAppDescriber struct { initAppDescriber func(string) error } -// NewWebAppDescriber instantiates a load balanced application. +// NewWebAppDescriber instantiates a load balanced application describer. func NewWebAppDescriber(project, app string) (*WebAppDescriber, error) { svc, err := store.New() if err != nil { @@ -115,14 +131,15 @@ func NewWebAppDescriberWithResources(project, app string) (*WebAppDescriber, err } // Describe returns info of a web app application. -func (d *WebAppDescriber) Describe() (*WebAppDesc, error) { +func (d *WebAppDescriber) Describe() (HumanJSONStringer, error) { environments, err := d.store.ListEnvironments(d.app.Project) if err != nil { return nil, fmt.Errorf("list environments: %w", err) } var routes []*WebAppRoute - var configs []*WebAppConfig + var configs []*AppConfig + var services []*ServiceDiscovery var envVars []*EnvVars for _, env := range environments { err := d.initAppDescriber(env.Name) @@ -139,13 +156,18 @@ func (d *WebAppDescriber) Describe() (*WebAppDesc, error) { if err != nil { return nil, fmt.Errorf("retrieve application deployment configuration: %w", err) } - configs = append(configs, &WebAppConfig{ + configs = append(configs, &AppConfig{ Environment: env.Name, Port: appParams[stack.LBWebAppContainerPortParamKey], Tasks: appParams[stack.AppTaskCountParamKey], CPU: appParams[stack.AppTaskCPUParamKey], Memory: appParams[stack.AppTaskMemoryParamKey], }) + services = appendServiceDiscovery(services, serviceDiscovery{ + AppName: d.app.Name, + Port: appParams[stack.LBWebAppContainerPortParamKey], + ProjectName: d.app.Project, + }, env.Name) webAppEnvVars, err := d.appDescriber.EnvVars() if err != nil { return nil, fmt.Errorf("retrieve environment variables: %w", err) @@ -175,13 +197,14 @@ func (d *WebAppDescriber) Describe() (*WebAppDesc, error) { } return &WebAppDesc{ - AppName: d.app.Name, - Type: d.app.Type, - Project: d.app.Project, - Configurations: configs, - Routes: routes, - Variables: envVars, - Resources: resources, + AppName: d.app.Name, + Type: d.app.Type, + Project: d.app.Project, + Configurations: configs, + Routes: routes, + ServiceDiscovery: services, + Variables: envVars, + Resources: resources, }, nil } @@ -228,8 +251,8 @@ type CfnResource struct { PhysicalID string `json:"physicalID"` } -// WebAppConfig contains serialized configuration parameters for a web application. -type WebAppConfig struct { +// AppConfig contains serialized configuration parameters for an application. +type AppConfig struct { Environment string `json:"environment"` Port string `json:"port"` Tasks string `json:"tasks"` @@ -243,15 +266,22 @@ type WebAppRoute struct { URL string `json:"url"` } +// ServiceDiscovery contains serialized service discovery info for an application. +type ServiceDiscovery struct { + Environment []string `json:"environment"` + Namespace string `json:"namespace"` +} + // WebAppDesc contains serialized parameters for a web application. type WebAppDesc struct { - AppName string `json:"appName"` - Type string `json:"type"` - Project string `json:"project"` - Configurations []*WebAppConfig `json:"configurations"` - Routes []*WebAppRoute `json:"routes"` - Variables []*EnvVars `json:"variables"` - Resources map[string][]*CfnResource `json:"resources,omitempty"` + AppName string `json:"appName"` + Type string `json:"type"` + Project string `json:"project"` + Configurations []*AppConfig `json:"configurations"` + Routes []*WebAppRoute `json:"routes"` + ServiceDiscovery []*ServiceDiscovery `json:"serviceDiscovery"` + Variables []*EnvVars `json:"variables"` + Resources map[string][]*CfnResource `json:"resources,omitempty"` } // JSONString returns the stringified WebApp struct with json format. @@ -284,6 +314,12 @@ func (w *WebAppDesc) HumanString() string { for _, route := range w.Routes { fmt.Fprintf(writer, " %s\t%s\n", route.Environment, route.URL) } + fmt.Fprintf(writer, color.Bold.Sprint("\nService Discovery\n\n")) + writer.Flush() + fmt.Fprintf(writer, " %s\t%s\n", "Environment", "Namespace") + for _, sd := range w.ServiceDiscovery { + fmt.Fprintf(writer, " %s\t%s\n", strings.Join(sd.Environment, ", "), sd.Namespace) + } fmt.Fprintf(writer, color.Bold.Sprint("\nVariables\n\n")) writer.Flush() fmt.Fprintf(writer, " %s\t%s\t%s\n", "Name", "Environment", "Value") @@ -351,3 +387,20 @@ func IsStackNotExistsErr(err error) bool { return true } } + +func appendServiceDiscovery(sds []*ServiceDiscovery, sd serviceDiscovery, env string) []*ServiceDiscovery { + exist := false + for _, s := range sds { + if s.Namespace == sd.String() { + s.Environment = append(s.Environment, env) + exist = true + } + } + if !exist { + sds = append(sds, &ServiceDiscovery{ + Environment: []string{env}, + Namespace: sd.String(), + }) + } + return sds +} diff --git a/internal/pkg/describe/lb_web_app_test.go b/internal/pkg/describe/lb_web_app_test.go index e2311a2cbc7..b9196483c98 100644 --- a/internal/pkg/describe/lb_web_app_test.go +++ b/internal/pkg/describe/lb_web_app_test.go @@ -276,13 +276,14 @@ func TestWebAppDescriber_Describe(t *testing.T) { ) }, wantedWebApp: &WebAppDesc{ - AppName: testApp, - Type: "", - Project: testProject, - Configurations: []*WebAppConfig(nil), - Routes: []*WebAppRoute(nil), - Variables: []*EnvVars(nil), - Resources: make(map[string][]*CfnResource), + AppName: testApp, + Type: "", + Project: testProject, + Configurations: []*AppConfig(nil), + Routes: []*WebAppRoute(nil), + ServiceDiscovery: []*ServiceDiscovery(nil), + Variables: []*EnvVars(nil), + Resources: make(map[string][]*CfnResource), }, }, "success": { @@ -301,7 +302,7 @@ func TestWebAppDescriber_Describe(t *testing.T) { stack.LBWebAppRulePathParamKey: testAppPath, }, nil), m.appDescriber.EXPECT().Params().Return(map[string]string{ - stack.LBWebAppContainerPortParamKey: "80", + stack.LBWebAppContainerPortParamKey: "5000", stack.AppTaskCountParamKey: "1", stack.AppTaskCPUParamKey: "256", stack.AppTaskMemoryParamKey: "512", @@ -346,12 +347,12 @@ func TestWebAppDescriber_Describe(t *testing.T) { AppName: testApp, Type: "", Project: testProject, - Configurations: []*WebAppConfig{ + Configurations: []*AppConfig{ { CPU: "256", Environment: "test", Memory: "512", - Port: "80", + Port: "5000", Tasks: "1", }, { @@ -372,6 +373,12 @@ func TestWebAppDescriber_Describe(t *testing.T) { URL: "http://abc.us-west-1.elb.amazonaws.com/*", }, }, + ServiceDiscovery: []*ServiceDiscovery{ + { + Environment: []string{"test", "prod"}, + Namespace: "http://jobs.phonetool.local:5000", + }, + }, Variables: []*EnvVars{ { Environment: "prod", diff --git a/internal/pkg/describe/mocks/mock_lb_web_app.go b/internal/pkg/describe/mocks/mock_lb_web_app.go index 150bbbe169b..55a010d99c9 100644 --- a/internal/pkg/describe/mocks/mock_lb_web_app.go +++ b/internal/pkg/describe/mocks/mock_lb_web_app.go @@ -162,3 +162,55 @@ func (mr *MockappDescriberMockRecorder) AppStackResources() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppStackResources", reflect.TypeOf((*MockappDescriber)(nil).AppStackResources)) } + +// MockHumanJSONStringer is a mock of HumanJSONStringer interface +type MockHumanJSONStringer struct { + ctrl *gomock.Controller + recorder *MockHumanJSONStringerMockRecorder +} + +// MockHumanJSONStringerMockRecorder is the mock recorder for MockHumanJSONStringer +type MockHumanJSONStringerMockRecorder struct { + mock *MockHumanJSONStringer +} + +// NewMockHumanJSONStringer creates a new mock instance +func NewMockHumanJSONStringer(ctrl *gomock.Controller) *MockHumanJSONStringer { + mock := &MockHumanJSONStringer{ctrl: ctrl} + mock.recorder = &MockHumanJSONStringerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockHumanJSONStringer) EXPECT() *MockHumanJSONStringerMockRecorder { + return m.recorder +} + +// HumanString mocks base method +func (m *MockHumanJSONStringer) HumanString() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HumanString") + ret0, _ := ret[0].(string) + return ret0 +} + +// HumanString indicates an expected call of HumanString +func (mr *MockHumanJSONStringerMockRecorder) HumanString() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanString", reflect.TypeOf((*MockHumanJSONStringer)(nil).HumanString)) +} + +// JSONString mocks base method +func (m *MockHumanJSONStringer) JSONString() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "JSONString") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// JSONString indicates an expected call of JSONString +func (mr *MockHumanJSONStringerMockRecorder) JSONString() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONString", reflect.TypeOf((*MockHumanJSONStringer)(nil).JSONString)) +} From f6003fc06a346319c24f33adaa867ea765c0ae8a Mon Sep 17 00:00:00 2001 From: penghaoh Date: Tue, 28 Apr 2020 11:21:29 -0700 Subject: [PATCH 2/3] Refactoring to simplify code and make struct private --- internal/pkg/cli/app_show.go | 4 +- internal/pkg/cli/app_show_test.go | 145 ++++------------- internal/pkg/cli/interfaces.go | 2 +- internal/pkg/cli/mocks/mock_interfaces.go | 28 ++-- internal/pkg/describe/backend_app.go | 113 ++++++-------- internal/pkg/describe/backend_app_test.go | 119 +++++++++++++- internal/pkg/describe/lb_web_app.go | 181 +++++++++++++--------- internal/pkg/describe/lb_web_app_test.go | 134 +++++++++++++++- 8 files changed, 438 insertions(+), 288 deletions(-) diff --git a/internal/pkg/cli/app_show.go b/internal/pkg/cli/app_show.go index c496a48e3a9..b399c89296c 100644 --- a/internal/pkg/cli/app_show.go +++ b/internal/pkg/cli/app_show.go @@ -36,7 +36,7 @@ type showAppOpts struct { w io.Writer storeSvc storeReader - describer appDescriber + describer describer ws wsAppReader initDescriber func(bool) error // Overriden in tests. } @@ -58,7 +58,7 @@ func newShowAppOpts(vars showAppVars) (*showAppOpts, error) { w: log.OutputWriter, } opts.initDescriber = func(enableResources bool) error { - var d appDescriber + var d describer app, err := opts.storeSvc.GetApplication(opts.ProjectName(), opts.appName) if err != nil { return err diff --git a/internal/pkg/cli/app_show_test.go b/internal/pkg/cli/app_show_test.go index ac0e3e97c44..a06668b930c 100644 --- a/internal/pkg/cli/app_show_test.go +++ b/internal/pkg/cli/app_show_test.go @@ -11,7 +11,6 @@ import ( "github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer" climocks "github.com/aws/amazon-ecs-cli-v2/internal/pkg/cli/mocks" - "github.com/aws/amazon-ecs-cli-v2/internal/pkg/describe" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" ) @@ -19,10 +18,23 @@ import ( type showAppMocks struct { storeSvc *climocks.MockstoreReader prompt *climocks.Mockprompter - describer *climocks.MockappDescriber + describer *climocks.Mockdescriber ws *climocks.MockwsAppReader } +type mockDescribeData struct { + data string + err error +} + +func (m *mockDescribeData) HumanString() string { + return m.data +} + +func (m *mockDescribeData) JSONString() (string, error) { + return m.data, m.err +} + func TestAppShow_Validate(t *testing.T) { testCases := map[string]struct { inputProject string @@ -370,72 +382,13 @@ func TestAppShow_Ask(t *testing.T) { func TestAppShow_Execute(t *testing.T) { projectName := "my-project" - webApp := describe.WebAppDesc{ - AppName: "my-app", - Configurations: []*describe.AppConfig{ - { - CPU: "256", - Environment: "test", - Memory: "512", - Port: "80", - Tasks: "1", - }, - { - CPU: "512", - Environment: "prod", - Memory: "1024", - Port: "5000", - Tasks: "3", - }, - }, - Project: "my-project", - Variables: []*describe.EnvVars{ - { - Environment: "prod", - Name: "ECS_CLI_ENVIRONMENT_NAME", - Value: "prod", - }, - { - Environment: "test", - Name: "ECS_CLI_ENVIRONMENT_NAME", - Value: "test", - }, - }, - Routes: []*describe.WebAppRoute{ - { - Environment: "test", - URL: "http://my-pr-Publi.us-west-2.elb.amazonaws.com/frontend", - }, - { - Environment: "prod", - URL: "http://my-pr-Publi.us-west-2.elb.amazonaws.com/backend", - }, - }, - ServiceDiscovery: []*describe.ServiceDiscovery{ - { - Environment: []string{"test", "prod"}, - Namespace: "http://my-app.my-project.local:5000", - }, - }, - Resources: map[string][]*describe.CfnResource{ - "test": []*describe.CfnResource{ - { - PhysicalID: "sg-0758ed6b233743530", - Type: "AWS::EC2::SecurityGroup", - }, - }, - "prod": []*describe.CfnResource{ - { - Type: "AWS::EC2::SecurityGroupIngress", - PhysicalID: "ContainerSecurityGroupIngressFromPublicALB", - }, - }, - }, + webApp := mockDescribeData{ + data: "mockData", + err: errors.New("some error"), } testCases := map[string]struct { - inputApp string - shouldOutputJSON bool - shouldOutputResources bool + inputApp string + shouldOutputJSON bool setupMocks func(mocks showAppMocks) @@ -447,10 +400,8 @@ func TestAppShow_Execute(t *testing.T) { m.describer.EXPECT().Describe().Times(0) }, }, - "prompt for all input for json output": { - inputApp: "my-app", - shouldOutputJSON: true, - shouldOutputResources: true, + "success": { + inputApp: "my-app", setupMocks: func(m showAppMocks) { gomock.InOrder( @@ -458,12 +409,11 @@ func TestAppShow_Execute(t *testing.T) { ) }, - wantedContent: "{\"appName\":\"my-app\",\"type\":\"\",\"project\":\"my-project\",\"configurations\":[{\"environment\":\"test\",\"port\":\"80\",\"tasks\":\"1\",\"cpu\":\"256\",\"memory\":\"512\"},{\"environment\":\"prod\",\"port\":\"5000\",\"tasks\":\"3\",\"cpu\":\"512\",\"memory\":\"1024\"}],\"routes\":[{\"environment\":\"test\",\"url\":\"http://my-pr-Publi.us-west-2.elb.amazonaws.com/frontend\"},{\"environment\":\"prod\",\"url\":\"http://my-pr-Publi.us-west-2.elb.amazonaws.com/backend\"}],\"serviceDiscovery\":[{\"environment\":[\"test\",\"prod\"],\"namespace\":\"http://my-app.my-project.local:5000\"}],\"variables\":[{\"environment\":\"prod\",\"name\":\"ECS_CLI_ENVIRONMENT_NAME\",\"value\":\"prod\"},{\"environment\":\"test\",\"name\":\"ECS_CLI_ENVIRONMENT_NAME\",\"value\":\"test\"}],\"resources\":{\"prod\":[{\"type\":\"AWS::EC2::SecurityGroupIngress\",\"physicalID\":\"ContainerSecurityGroupIngressFromPublicALB\"}],\"test\":[{\"type\":\"AWS::EC2::SecurityGroup\",\"physicalID\":\"sg-0758ed6b233743530\"}]}}\n", + wantedContent: "mockData", }, - "prompt for all input for human output": { - inputApp: "my-app", - shouldOutputJSON: false, - shouldOutputResources: true, + "return error if fail to generate JSON output": { + inputApp: "my-app", + shouldOutputJSON: true, setupMocks: func(m showAppMocks) { gomock.InOrder( @@ -471,43 +421,7 @@ func TestAppShow_Execute(t *testing.T) { ) }, - wantedContent: `About - - Project my-project - Name my-app - Type - -Configurations - - Environment Tasks CPU (vCPU) Memory (MiB) Port - test 1 0.25 512 80 - prod 3 0.5 1024 5000 - -Routes - - Environment URL - test http://my-pr-Publi.us-west-2.elb.amazonaws.com/frontend - prod http://my-pr-Publi.us-west-2.elb.amazonaws.com/backend - -Service Discovery - - Environment Namespace - test, prod http://my-app.my-project.local:5000 - -Variables - - Name Environment Value - ECS_CLI_ENVIRONMENT_NAME prod prod - - test test - -Resources - - test - AWS::EC2::SecurityGroup sg-0758ed6b233743530 - - prod - AWS::EC2::SecurityGroupIngress ContainerSecurityGroupIngressFromPublicALB -`, + wantedError: fmt.Errorf("some error"), }, "return error if fail to describe application": { inputApp: "my-app", @@ -528,7 +442,7 @@ Resources defer ctrl.Finish() b := &bytes.Buffer{} - mockAppDescriber := climocks.NewMockappDescriber(ctrl) + mockAppDescriber := climocks.NewMockdescriber(ctrl) mocks := showAppMocks{ describer: mockAppDescriber, @@ -538,9 +452,8 @@ Resources showApps := &showAppOpts{ showAppVars: showAppVars{ - appName: tc.inputApp, - shouldOutputJSON: tc.shouldOutputJSON, - shouldOutputResources: tc.shouldOutputResources, + appName: tc.inputApp, + shouldOutputJSON: tc.shouldOutputJSON, GlobalOpts: &GlobalOpts{ projectName: projectName, }, diff --git a/internal/pkg/cli/interfaces.go b/internal/pkg/cli/interfaces.go index ba8772f60e5..29145cef6bf 100644 --- a/internal/pkg/cli/interfaces.go +++ b/internal/pkg/cli/interfaces.go @@ -91,7 +91,7 @@ type sessionProvider interface { sessionFromRoleProvider } -type appDescriber interface { +type describer interface { Describe() (describe.HumanJSONStringer, error) } diff --git a/internal/pkg/cli/mocks/mock_interfaces.go b/internal/pkg/cli/mocks/mock_interfaces.go index b205b99582a..1d731dc5df9 100644 --- a/internal/pkg/cli/mocks/mock_interfaces.go +++ b/internal/pkg/cli/mocks/mock_interfaces.go @@ -825,31 +825,31 @@ func (mr *MocksessionProviderMockRecorder) FromRole(roleARN, region interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FromRole", reflect.TypeOf((*MocksessionProvider)(nil).FromRole), roleARN, region) } -// MockappDescriber is a mock of appDescriber interface -type MockappDescriber struct { +// Mockdescriber is a mock of describer interface +type Mockdescriber struct { ctrl *gomock.Controller - recorder *MockappDescriberMockRecorder + recorder *MockdescriberMockRecorder } -// MockappDescriberMockRecorder is the mock recorder for MockappDescriber -type MockappDescriberMockRecorder struct { - mock *MockappDescriber +// MockdescriberMockRecorder is the mock recorder for Mockdescriber +type MockdescriberMockRecorder struct { + mock *Mockdescriber } -// NewMockappDescriber creates a new mock instance -func NewMockappDescriber(ctrl *gomock.Controller) *MockappDescriber { - mock := &MockappDescriber{ctrl: ctrl} - mock.recorder = &MockappDescriberMockRecorder{mock} +// NewMockdescriber creates a new mock instance +func NewMockdescriber(ctrl *gomock.Controller) *Mockdescriber { + mock := &Mockdescriber{ctrl: ctrl} + mock.recorder = &MockdescriberMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use -func (m *MockappDescriber) EXPECT() *MockappDescriberMockRecorder { +func (m *Mockdescriber) EXPECT() *MockdescriberMockRecorder { return m.recorder } // Describe mocks base method -func (m *MockappDescriber) Describe() (describe.HumanJSONStringer, error) { +func (m *Mockdescriber) Describe() (describe.HumanJSONStringer, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Describe") ret0, _ := ret[0].(describe.HumanJSONStringer) @@ -858,9 +858,9 @@ func (m *MockappDescriber) Describe() (describe.HumanJSONStringer, error) { } // Describe indicates an expected call of Describe -func (mr *MockappDescriberMockRecorder) Describe() *gomock.Call { +func (mr *MockdescriberMockRecorder) Describe() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Describe", reflect.TypeOf((*MockappDescriber)(nil).Describe)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Describe", reflect.TypeOf((*Mockdescriber)(nil).Describe)) } // MockstoreReader is a mock of storeReader interface diff --git a/internal/pkg/describe/backend_app.go b/internal/pkg/describe/backend_app.go index 9ccf986d492..1a989e65c6b 100644 --- a/internal/pkg/describe/backend_app.go +++ b/internal/pkg/describe/backend_app.go @@ -8,7 +8,6 @@ import ( "encoding/json" "fmt" "sort" - "strings" "text/tabwriter" "github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer" @@ -63,11 +62,17 @@ func NewBackendAppDescriberWithResources(project, app string) (*BackendAppDescri return d, nil } +// URI is used to make BackendAppDescriber have the same signature as WebAppDescriber +func (d *BackendAppDescriber) URI(envName string) (string, error) { + s := serviceDiscovery{} + return s.String(), nil +} + // Describe returns info of a backend application. func (d *BackendAppDescriber) Describe() (HumanJSONStringer, error) { environments, err := d.store.ListEnvironments(d.app.Project) if err != nil { - return nil, fmt.Errorf("list environments: %w", err) + return nil, fmt.Errorf("list environments for project %s: %w", d.app.Project, err) } var configs []*AppConfig @@ -79,29 +84,29 @@ func (d *BackendAppDescriber) Describe() (HumanJSONStringer, error) { return nil, err } appParams, err := d.appDescriber.Params() - if err == nil { - services = appendServiceDiscovery(services, serviceDiscovery{ - AppName: d.app.Name, - Port: appParams[stack.LBWebAppContainerPortParamKey], - ProjectName: d.app.Project, - }, env.Name) - configs = append(configs, &AppConfig{ - Environment: env.Name, - Port: appParams[stack.LBWebAppContainerPortParamKey], - Tasks: appParams[stack.AppTaskCountParamKey], - CPU: appParams[stack.AppTaskCPUParamKey], - Memory: appParams[stack.AppTaskMemoryParamKey], - }) - backendAppEnvVars, err := d.appDescriber.EnvVars() - if err != nil { - return nil, fmt.Errorf("retrieve environment variables: %w", err) - } - envVars = append(envVars, flattenEnvVars(env.Name, backendAppEnvVars)...) + if err != nil && !IsStackNotExistsErr(err) { + return nil, fmt.Errorf("retrieve application deployment configuration: %w", err) + } + if err != nil { continue } - if !IsStackNotExistsErr(err) { - return nil, fmt.Errorf("retrieve application deployment configuration: %w", err) + services = appendServiceDiscovery(services, serviceDiscovery{ + AppName: d.app.Name, + Port: appParams[stack.LBWebAppContainerPortParamKey], + ProjectName: d.app.Project, + }, env.Name) + configs = append(configs, &AppConfig{ + Environment: env.Name, + Port: appParams[stack.LBWebAppContainerPortParamKey], + Tasks: appParams[stack.AppTaskCountParamKey], + CPU: appParams[stack.AppTaskCPUParamKey], + Memory: appParams[stack.AppTaskMemoryParamKey], + }) + backendAppEnvVars, err := d.appDescriber.EnvVars() + if err != nil { + return nil, fmt.Errorf("retrieve environment variables: %w", err) } + envVars = append(envVars, flattenEnvVars(env.Name, backendAppEnvVars)...) } sort.SliceStable(envVars, func(i, j int) bool { return envVars[i].Environment < envVars[j].Environment }) sort.SliceStable(envVars, func(i, j int) bool { return envVars[i].Name < envVars[j].Name }) @@ -120,7 +125,7 @@ func (d *BackendAppDescriber) Describe() (HumanJSONStringer, error) { } } - return &BackendAppDesc{ + return &backendAppDesc{ AppName: d.app.Name, Type: d.app.Type, Project: d.app.Project, @@ -131,19 +136,19 @@ func (d *BackendAppDescriber) Describe() (HumanJSONStringer, error) { }, nil } -// BackendAppDesc contains serialized parameters for a backend application. -type BackendAppDesc struct { - AppName string `json:"appName"` - Type string `json:"type"` - Project string `json:"project"` - Configurations []*AppConfig `json:"configurations"` - ServiceDiscovery []*ServiceDiscovery `json:"serviceDiscovery"` - Variables []*EnvVars `json:"variables"` - Resources map[string][]*CfnResource `json:"resources,omitempty"` +// backendAppDesc contains serialized parameters for a backend application. +type backendAppDesc struct { + AppName string `json:"appName"` + Type string `json:"type"` + Project string `json:"project"` + Configurations configurations `json:"configurations"` + ServiceDiscovery serviceDiscoveries `json:"serviceDiscovery"` + Variables envVars `json:"variables"` + Resources cfnResources `json:"resources,omitempty"` } // JSONString returns the stringified BackendApp struct with json format. -func (w *BackendAppDesc) JSONString() (string, error) { +func (w *backendAppDesc) JSONString() (string, error) { b, err := json.Marshal(w) if err != nil { return "", fmt.Errorf("marshal applications: %w", err) @@ -152,7 +157,7 @@ func (w *BackendAppDesc) JSONString() (string, error) { } // HumanString returns the stringified BackendApp struct with human readable format. -func (w *BackendAppDesc) HumanString() string { +func (w *backendAppDesc) HumanString() string { var b bytes.Buffer writer := tabwriter.NewWriter(&b, minCellWidth, tabWidth, cellPaddingWidth, paddingChar, noAdditionalFormatting) fmt.Fprintf(writer, color.Bold.Sprint("About\n\n")) @@ -163,52 +168,22 @@ func (w *BackendAppDesc) HumanString() string { fmt.Fprintf(writer, color.Bold.Sprint("\nConfigurations\n\n")) writer.Flush() fmt.Fprintf(writer, " %s\t%s\t%s\t%s\t%s\n", "Environment", "Tasks", "CPU (vCPU)", "Memory (MiB)", "Port") - for _, config := range w.Configurations { - fmt.Fprintf(writer, " %s\t%s\t%s\t%s\t%s\n", config.Environment, config.Tasks, cpuToString(config.CPU), config.Memory, config.Port) - } + w.Configurations.humanString(writer) fmt.Fprintf(writer, color.Bold.Sprint("\nService Discovery\n\n")) writer.Flush() fmt.Fprintf(writer, " %s\t%s\n", "Environment", "Namespace") - for _, sd := range w.ServiceDiscovery { - fmt.Fprintf(writer, " %s\t%s\n", strings.Join(sd.Environment, ", "), sd.Namespace) - } + w.ServiceDiscovery.humanString(writer) fmt.Fprintf(writer, color.Bold.Sprint("\nVariables\n\n")) writer.Flush() fmt.Fprintf(writer, " %s\t%s\t%s\n", "Name", "Environment", "Value") - var prevName string - var prevValue string - for _, variable := range w.Variables { - // Instead of re-writing the same variable value, we replace it with "-" to reduce text. - if variable.Name != prevName { - if variable.Value != prevValue { - fmt.Fprintf(writer, " %s\t%s\t%s\n", variable.Name, variable.Environment, variable.Value) - } else { - fmt.Fprintf(writer, " %s\t%s\t-\n", variable.Name, variable.Environment) - } - } else { - if variable.Value != prevValue { - fmt.Fprintf(writer, " -\t%s\t%s\n", variable.Environment, variable.Value) - } else { - fmt.Fprintf(writer, " -\t%s\t-\n", variable.Environment) - } - } - prevName = variable.Name - prevValue = variable.Value - } + w.Variables.humanString(writer) if len(w.Resources) != 0 { fmt.Fprintf(writer, color.Bold.Sprint("\nResources\n")) writer.Flush() // Go maps don't have a guaranteed order. - // Show the resources by the order of environments displayed under Routes for a consistent view. - for _, config := range w.Configurations { - env := config.Environment - resources := w.Resources[env] - fmt.Fprintf(writer, "\n %s\n", env) - for _, resource := range resources { - fmt.Fprintf(writer, " %s\t%s\n", resource.Type, resource.PhysicalID) - } - } + // Show the resources by the order of environments displayed under Configurations for a consistent view. + w.Resources.humanString(writer, w.Configurations) } writer.Flush() return b.String() diff --git a/internal/pkg/describe/backend_app_test.go b/internal/pkg/describe/backend_app_test.go index f1581e3173b..5e10e655b54 100644 --- a/internal/pkg/describe/backend_app_test.go +++ b/internal/pkg/describe/backend_app_test.go @@ -47,7 +47,7 @@ func TestBackendAppDescriber_Describe(t *testing.T) { setupMocks func(mocks backendAppDescriberMocks) - wantedBackendApp *BackendAppDesc + wantedBackendApp *backendAppDesc wantedError error }{ "return error if fail to list environment": { @@ -56,7 +56,7 @@ func TestBackendAppDescriber_Describe(t *testing.T) { m.storeSvc.EXPECT().ListEnvironments(testProject).Return(nil, mockErr), ) }, - wantedError: fmt.Errorf("list environments: some error"), + wantedError: fmt.Errorf("list environments for project phonetool: some error"), }, "return error if fail to retrieve application deployment configuration": { setupMocks: func(m backendAppDescriberMocks) { @@ -97,7 +97,7 @@ func TestBackendAppDescriber_Describe(t *testing.T) { m.appDescriber.EXPECT().AppStackResources().Return(nil, mockNotExistErr), ) }, - wantedBackendApp: &BackendAppDesc{ + wantedBackendApp: &backendAppDesc{ AppName: testApp, Type: "", Project: testProject, @@ -152,7 +152,7 @@ func TestBackendAppDescriber_Describe(t *testing.T) { }, nil), ) }, - wantedBackendApp: &BackendAppDesc{ + wantedBackendApp: &backendAppDesc{ AppName: testApp, Type: "", Project: testProject, @@ -175,7 +175,7 @@ func TestBackendAppDescriber_Describe(t *testing.T) { ServiceDiscovery: []*ServiceDiscovery{ { Environment: []string{"test", "prod"}, - Namespace: "http://jobs.phonetool.local:5000", + Namespace: "jobs.phonetool.local:5000", }, }, Variables: []*EnvVars{ @@ -246,3 +246,112 @@ func TestBackendAppDescriber_Describe(t *testing.T) { }) } } + +func TestBackendAppDesc_String(t *testing.T) { + testCases := map[string]struct { + wantedHumanString string + wantedJSONString string + }{ + "correct output": { + wantedHumanString: `About + + Project my-project + Name my-app + Type Backend App + +Configurations + + Environment Tasks CPU (vCPU) Memory (MiB) Port + test 1 0.25 512 80 + prod 3 0.5 1024 5000 + +Service Discovery + + Environment Namespace + test, prod http://my-app.my-project.local:5000 + +Variables + + Name Environment Value + ECS_CLI_ENVIRONMENT_NAME prod prod + - test test + +Resources + + test + AWS::EC2::SecurityGroup sg-0758ed6b233743530 + + prod + AWS::EC2::SecurityGroupIngress ContainerSecurityGroupIngressFromPublicALB +`, + wantedJSONString: "{\"appName\":\"my-app\",\"type\":\"Backend App\",\"project\":\"my-project\",\"configurations\":[{\"environment\":\"test\",\"port\":\"80\",\"tasks\":\"1\",\"cpu\":\"256\",\"memory\":\"512\"},{\"environment\":\"prod\",\"port\":\"5000\",\"tasks\":\"3\",\"cpu\":\"512\",\"memory\":\"1024\"}],\"serviceDiscovery\":[{\"environment\":[\"test\",\"prod\"],\"namespace\":\"http://my-app.my-project.local:5000\"}],\"variables\":[{\"environment\":\"prod\",\"name\":\"ECS_CLI_ENVIRONMENT_NAME\",\"value\":\"prod\"},{\"environment\":\"test\",\"name\":\"ECS_CLI_ENVIRONMENT_NAME\",\"value\":\"test\"}],\"resources\":{\"prod\":[{\"type\":\"AWS::EC2::SecurityGroupIngress\",\"physicalID\":\"ContainerSecurityGroupIngressFromPublicALB\"}],\"test\":[{\"type\":\"AWS::EC2::SecurityGroup\",\"physicalID\":\"sg-0758ed6b233743530\"}]}}\n", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + config := []*AppConfig{ + { + CPU: "256", + Environment: "test", + Memory: "512", + Port: "80", + Tasks: "1", + }, + { + CPU: "512", + Environment: "prod", + Memory: "1024", + Port: "5000", + Tasks: "3", + }, + } + envVars := []*EnvVars{ + { + Environment: "prod", + Name: "ECS_CLI_ENVIRONMENT_NAME", + Value: "prod", + }, + { + Environment: "test", + Name: "ECS_CLI_ENVIRONMENT_NAME", + Value: "test", + }, + } + sds := []*ServiceDiscovery{ + { + Environment: []string{"test", "prod"}, + Namespace: "http://my-app.my-project.local:5000", + }, + } + resources := map[string][]*CfnResource{ + "test": []*CfnResource{ + { + PhysicalID: "sg-0758ed6b233743530", + Type: "AWS::EC2::SecurityGroup", + }, + }, + "prod": []*CfnResource{ + { + Type: "AWS::EC2::SecurityGroupIngress", + PhysicalID: "ContainerSecurityGroupIngressFromPublicALB", + }, + }, + } + backendApp := &backendAppDesc{ + AppName: "my-app", + Type: "Backend App", + Configurations: config, + Project: "my-project", + Variables: envVars, + ServiceDiscovery: sds, + Resources: resources, + } + human := backendApp.HumanString() + json, _ := backendApp.JSONString() + + require.Equal(t, tc.wantedHumanString, human) + require.Equal(t, tc.wantedJSONString, json) + }) + } +} diff --git a/internal/pkg/describe/lb_web_app.go b/internal/pkg/describe/lb_web_app.go index 458be821d81..1467ac77aa3 100644 --- a/internal/pkg/describe/lb_web_app.go +++ b/internal/pkg/describe/lb_web_app.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "sort" "strconv" "strings" @@ -62,7 +63,7 @@ type serviceDiscovery struct { } func (s *serviceDiscovery) String() string { - return fmt.Sprintf("http://%s.%s.local:%s", s.AppName, s.ProjectName, s.Port) + return fmt.Sprintf("%s.%s.local:%s", s.AppName, s.ProjectName, s.Port) } type storeSvc interface { @@ -147,37 +148,37 @@ func (d *WebAppDescriber) Describe() (HumanJSONStringer, error) { return nil, err } webAppURI, err := d.URI(env.Name) - if err == nil { - routes = append(routes, &WebAppRoute{ - Environment: env.Name, - URL: webAppURI, - }) - appParams, err := d.appDescriber.Params() - if err != nil { - return nil, fmt.Errorf("retrieve application deployment configuration: %w", err) - } - configs = append(configs, &AppConfig{ - Environment: env.Name, - Port: appParams[stack.LBWebAppContainerPortParamKey], - Tasks: appParams[stack.AppTaskCountParamKey], - CPU: appParams[stack.AppTaskCPUParamKey], - Memory: appParams[stack.AppTaskMemoryParamKey], - }) - services = appendServiceDiscovery(services, serviceDiscovery{ - AppName: d.app.Name, - Port: appParams[stack.LBWebAppContainerPortParamKey], - ProjectName: d.app.Project, - }, env.Name) - webAppEnvVars, err := d.appDescriber.EnvVars() - if err != nil { - return nil, fmt.Errorf("retrieve environment variables: %w", err) - } - envVars = append(envVars, flattenEnvVars(env.Name, webAppEnvVars)...) + if err != nil && !IsStackNotExistsErr(err) { + return nil, fmt.Errorf("retrieve application URI: %w", err) + } + if err != nil { continue } - if !IsStackNotExistsErr(err) { - return nil, fmt.Errorf("retrieve application URI: %w", err) + routes = append(routes, &WebAppRoute{ + Environment: env.Name, + URL: webAppURI, + }) + appParams, err := d.appDescriber.Params() + if err != nil { + return nil, fmt.Errorf("retrieve application deployment configuration: %w", err) + } + configs = append(configs, &AppConfig{ + Environment: env.Name, + Port: appParams[stack.LBWebAppContainerPortParamKey], + Tasks: appParams[stack.AppTaskCountParamKey], + CPU: appParams[stack.AppTaskCPUParamKey], + Memory: appParams[stack.AppTaskMemoryParamKey], + }) + services = appendServiceDiscovery(services, serviceDiscovery{ + AppName: d.app.Name, + Port: appParams[stack.LBWebAppContainerPortParamKey], + ProjectName: d.app.Project, + }, env.Name) + webAppEnvVars, err := d.appDescriber.EnvVars() + if err != nil { + return nil, fmt.Errorf("retrieve environment variables: %w", err) } + envVars = append(envVars, flattenEnvVars(env.Name, webAppEnvVars)...) } sort.SliceStable(envVars, func(i, j int) bool { return envVars[i].Environment < envVars[j].Environment }) sort.SliceStable(envVars, func(i, j int) bool { return envVars[i].Name < envVars[j].Name }) @@ -196,7 +197,7 @@ func (d *WebAppDescriber) Describe() (HumanJSONStringer, error) { } } - return &WebAppDesc{ + return &webAppDesc{ AppName: d.app.Name, Type: d.app.Type, Project: d.app.Project, @@ -245,12 +246,52 @@ type EnvVars struct { Value string `json:"value"` } +type envVars []*EnvVars + +func (e envVars) humanString(w io.Writer) { + var prevName string + var prevValue string + for _, variable := range e { + // Instead of re-writing the same variable value, we replace it with "-" to reduce text. + if variable.Name != prevName { + if variable.Value != prevValue { + fmt.Fprintf(w, " %s\t%s\t%s\n", variable.Name, variable.Environment, variable.Value) + } else { + fmt.Fprintf(w, " %s\t%s\t-\n", variable.Name, variable.Environment) + } + } else { + if variable.Value != prevValue { + fmt.Fprintf(w, " -\t%s\t%s\n", variable.Environment, variable.Value) + } else { + fmt.Fprintf(w, " -\t%s\t-\n", variable.Environment) + } + } + prevName = variable.Name + prevValue = variable.Value + } +} + // CfnResource contains application resources created by cloudformation. type CfnResource struct { Type string `json:"type"` PhysicalID string `json:"physicalID"` } +type cfnResources map[string][]*CfnResource + +func (c cfnResources) humanString(w io.Writer, configs []*AppConfig) { + // Go maps don't have a guaranteed order. + // Show the resources by the order of environments displayed under Configuration for a consistent view. + for _, config := range configs { + env := config.Environment + resources := c[env] + fmt.Fprintf(w, "\n %s\n", env) + for _, resource := range resources { + fmt.Fprintf(w, " %s\t%s\n", resource.Type, resource.PhysicalID) + } + } +} + // AppConfig contains serialized configuration parameters for an application. type AppConfig struct { Environment string `json:"environment"` @@ -260,6 +301,14 @@ type AppConfig struct { Memory string `json:"memory"` } +type configurations []*AppConfig + +func (c configurations) humanString(w io.Writer) { + for _, config := range c { + fmt.Fprintf(w, " %s\t%s\t%s\t%s\t%s\n", config.Environment, config.Tasks, cpuToString(config.CPU), config.Memory, config.Port) + } +} + // WebAppRoute contains serialized route parameters for a web application. type WebAppRoute struct { Environment string `json:"environment"` @@ -272,20 +321,28 @@ type ServiceDiscovery struct { Namespace string `json:"namespace"` } -// WebAppDesc contains serialized parameters for a web application. -type WebAppDesc struct { - AppName string `json:"appName"` - Type string `json:"type"` - Project string `json:"project"` - Configurations []*AppConfig `json:"configurations"` - Routes []*WebAppRoute `json:"routes"` - ServiceDiscovery []*ServiceDiscovery `json:"serviceDiscovery"` - Variables []*EnvVars `json:"variables"` - Resources map[string][]*CfnResource `json:"resources,omitempty"` +type serviceDiscoveries []*ServiceDiscovery + +func (s serviceDiscoveries) humanString(w io.Writer) { + for _, sd := range s { + fmt.Fprintf(w, " %s\t%s\n", strings.Join(sd.Environment, ", "), sd.Namespace) + } +} + +// webAppDesc contains serialized parameters for a web application. +type webAppDesc struct { + AppName string `json:"appName"` + Type string `json:"type"` + Project string `json:"project"` + Configurations configurations `json:"configurations"` + Routes []*WebAppRoute `json:"routes"` + ServiceDiscovery serviceDiscoveries `json:"serviceDiscovery"` + Variables envVars `json:"variables"` + Resources cfnResources `json:"resources,omitempty"` } // JSONString returns the stringified WebApp struct with json format. -func (w *WebAppDesc) JSONString() (string, error) { +func (w *webAppDesc) JSONString() (string, error) { b, err := json.Marshal(w) if err != nil { return "", fmt.Errorf("marshal applications: %w", err) @@ -294,7 +351,7 @@ func (w *WebAppDesc) JSONString() (string, error) { } // HumanString returns the stringified WebApp struct with human readable format. -func (w *WebAppDesc) HumanString() string { +func (w *webAppDesc) HumanString() string { var b bytes.Buffer writer := tabwriter.NewWriter(&b, minCellWidth, tabWidth, cellPaddingWidth, paddingChar, noAdditionalFormatting) fmt.Fprintf(writer, color.Bold.Sprint("About\n\n")) @@ -305,9 +362,7 @@ func (w *WebAppDesc) HumanString() string { fmt.Fprintf(writer, color.Bold.Sprint("\nConfigurations\n\n")) writer.Flush() fmt.Fprintf(writer, " %s\t%s\t%s\t%s\t%s\n", "Environment", "Tasks", "CPU (vCPU)", "Memory (MiB)", "Port") - for _, config := range w.Configurations { - fmt.Fprintf(writer, " %s\t%s\t%s\t%s\t%s\n", config.Environment, config.Tasks, cpuToString(config.CPU), config.Memory, config.Port) - } + w.Configurations.humanString(writer) fmt.Fprintf(writer, color.Bold.Sprint("\nRoutes\n\n")) writer.Flush() fmt.Fprintf(writer, " %s\t%s\n", "Environment", "URL") @@ -317,46 +372,18 @@ func (w *WebAppDesc) HumanString() string { fmt.Fprintf(writer, color.Bold.Sprint("\nService Discovery\n\n")) writer.Flush() fmt.Fprintf(writer, " %s\t%s\n", "Environment", "Namespace") - for _, sd := range w.ServiceDiscovery { - fmt.Fprintf(writer, " %s\t%s\n", strings.Join(sd.Environment, ", "), sd.Namespace) - } + w.ServiceDiscovery.humanString(writer) fmt.Fprintf(writer, color.Bold.Sprint("\nVariables\n\n")) writer.Flush() fmt.Fprintf(writer, " %s\t%s\t%s\n", "Name", "Environment", "Value") - var prevName string - var prevValue string - for _, variable := range w.Variables { - // Instead of re-writing the same variable value, we replace it with "-" to reduce text. - if variable.Name != prevName { - if variable.Value != prevValue { - fmt.Fprintf(writer, " %s\t%s\t%s\n", variable.Name, variable.Environment, variable.Value) - } else { - fmt.Fprintf(writer, " %s\t%s\t-\n", variable.Name, variable.Environment) - } - } else { - if variable.Value != prevValue { - fmt.Fprintf(writer, " -\t%s\t%s\n", variable.Environment, variable.Value) - } else { - fmt.Fprintf(writer, " -\t%s\t-\n", variable.Environment) - } - } - prevName = variable.Name - prevValue = variable.Value - } + w.Variables.humanString(writer) if len(w.Resources) != 0 { fmt.Fprintf(writer, color.Bold.Sprint("\nResources\n")) writer.Flush() // Go maps don't have a guaranteed order. - // Show the resources by the order of environments displayed under Routes for a consistent view. - for _, route := range w.Routes { - env := route.Environment - resources := w.Resources[env] - fmt.Fprintf(writer, "\n %s\n", env) - for _, resource := range resources { - fmt.Fprintf(writer, " %s\t%s\n", resource.Type, resource.PhysicalID) - } - } + // Show the resources by the order of environments displayed under Configuration for a consistent view. + w.Resources.humanString(writer, w.Configurations) } writer.Flush() return b.String() diff --git a/internal/pkg/describe/lb_web_app_test.go b/internal/pkg/describe/lb_web_app_test.go index b9196483c98..cff1643b20e 100644 --- a/internal/pkg/describe/lb_web_app_test.go +++ b/internal/pkg/describe/lb_web_app_test.go @@ -191,7 +191,7 @@ func TestWebAppDescriber_Describe(t *testing.T) { setupMocks func(mocks webAppDescriberMocks) - wantedWebApp *WebAppDesc + wantedWebApp *webAppDesc wantedError error }{ "return error if fail to list environment": { @@ -275,7 +275,7 @@ func TestWebAppDescriber_Describe(t *testing.T) { m.appDescriber.EXPECT().AppStackResources().Return(nil, mockNotExistErr), ) }, - wantedWebApp: &WebAppDesc{ + wantedWebApp: &webAppDesc{ AppName: testApp, Type: "", Project: testProject, @@ -343,7 +343,7 @@ func TestWebAppDescriber_Describe(t *testing.T) { }, nil), ) }, - wantedWebApp: &WebAppDesc{ + wantedWebApp: &webAppDesc{ AppName: testApp, Type: "", Project: testProject, @@ -376,7 +376,7 @@ func TestWebAppDescriber_Describe(t *testing.T) { ServiceDiscovery: []*ServiceDiscovery{ { Environment: []string{"test", "prod"}, - Namespace: "http://jobs.phonetool.local:5000", + Namespace: "jobs.phonetool.local:5000", }, }, Variables: []*EnvVars{ @@ -447,3 +447,129 @@ func TestWebAppDescriber_Describe(t *testing.T) { }) } } + +func TestWebAppDesc_String(t *testing.T) { + testCases := map[string]struct { + wantedHumanString string + wantedJSONString string + }{ + "correct output": { + wantedHumanString: `About + + Project my-project + Name my-app + Type Load Balanced Web App + +Configurations + + Environment Tasks CPU (vCPU) Memory (MiB) Port + test 1 0.25 512 80 + prod 3 0.5 1024 5000 + +Routes + + Environment URL + test http://my-pr-Publi.us-west-2.elb.amazonaws.com/frontend + prod http://my-pr-Publi.us-west-2.elb.amazonaws.com/backend + +Service Discovery + + Environment Namespace + test, prod http://my-app.my-project.local:5000 + +Variables + + Name Environment Value + ECS_CLI_ENVIRONMENT_NAME prod prod + - test test + +Resources + + test + AWS::EC2::SecurityGroup sg-0758ed6b233743530 + + prod + AWS::EC2::SecurityGroupIngress ContainerSecurityGroupIngressFromPublicALB +`, + wantedJSONString: "{\"appName\":\"my-app\",\"type\":\"Load Balanced Web App\",\"project\":\"my-project\",\"configurations\":[{\"environment\":\"test\",\"port\":\"80\",\"tasks\":\"1\",\"cpu\":\"256\",\"memory\":\"512\"},{\"environment\":\"prod\",\"port\":\"5000\",\"tasks\":\"3\",\"cpu\":\"512\",\"memory\":\"1024\"}],\"routes\":[{\"environment\":\"test\",\"url\":\"http://my-pr-Publi.us-west-2.elb.amazonaws.com/frontend\"},{\"environment\":\"prod\",\"url\":\"http://my-pr-Publi.us-west-2.elb.amazonaws.com/backend\"}],\"serviceDiscovery\":[{\"environment\":[\"test\",\"prod\"],\"namespace\":\"http://my-app.my-project.local:5000\"}],\"variables\":[{\"environment\":\"prod\",\"name\":\"ECS_CLI_ENVIRONMENT_NAME\",\"value\":\"prod\"},{\"environment\":\"test\",\"name\":\"ECS_CLI_ENVIRONMENT_NAME\",\"value\":\"test\"}],\"resources\":{\"prod\":[{\"type\":\"AWS::EC2::SecurityGroupIngress\",\"physicalID\":\"ContainerSecurityGroupIngressFromPublicALB\"}],\"test\":[{\"type\":\"AWS::EC2::SecurityGroup\",\"physicalID\":\"sg-0758ed6b233743530\"}]}}\n", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + config := []*AppConfig{ + { + CPU: "256", + Environment: "test", + Memory: "512", + Port: "80", + Tasks: "1", + }, + { + CPU: "512", + Environment: "prod", + Memory: "1024", + Port: "5000", + Tasks: "3", + }, + } + envVars := []*EnvVars{ + { + Environment: "prod", + Name: "ECS_CLI_ENVIRONMENT_NAME", + Value: "prod", + }, + { + Environment: "test", + Name: "ECS_CLI_ENVIRONMENT_NAME", + Value: "test", + }, + } + routes := []*WebAppRoute{ + { + Environment: "test", + URL: "http://my-pr-Publi.us-west-2.elb.amazonaws.com/frontend", + }, + { + Environment: "prod", + URL: "http://my-pr-Publi.us-west-2.elb.amazonaws.com/backend", + }, + } + sds := []*ServiceDiscovery{ + { + Environment: []string{"test", "prod"}, + Namespace: "http://my-app.my-project.local:5000", + }, + } + resources := map[string][]*CfnResource{ + "test": []*CfnResource{ + { + PhysicalID: "sg-0758ed6b233743530", + Type: "AWS::EC2::SecurityGroup", + }, + }, + "prod": []*CfnResource{ + { + Type: "AWS::EC2::SecurityGroupIngress", + PhysicalID: "ContainerSecurityGroupIngressFromPublicALB", + }, + }, + } + webApp := &webAppDesc{ + AppName: "my-app", + Type: "Load Balanced Web App", + Configurations: config, + Project: "my-project", + Variables: envVars, + Routes: routes, + ServiceDiscovery: sds, + Resources: resources, + } + human := webApp.HumanString() + json, _ := webApp.JSONString() + + require.Equal(t, tc.wantedHumanString, human) + require.Equal(t, tc.wantedJSONString, json) + }) + } +} From cd39b1e31425bdc850b5dc8f8d7cf03729b91a58 Mon Sep 17 00:00:00 2001 From: penghaoh Date: Tue, 28 Apr 2020 11:52:20 -0700 Subject: [PATCH 3/3] Fix BackendAppDescriber.URI --- internal/pkg/describe/backend_app.go | 29 ++++++++++++++++++---------- internal/pkg/describe/lb_web_app.go | 16 +++++++-------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/internal/pkg/describe/backend_app.go b/internal/pkg/describe/backend_app.go index 1a989e65c6b..0582a368886 100644 --- a/internal/pkg/describe/backend_app.go +++ b/internal/pkg/describe/backend_app.go @@ -62,9 +62,21 @@ func NewBackendAppDescriberWithResources(project, app string) (*BackendAppDescri return d, nil } -// URI is used to make BackendAppDescriber have the same signature as WebAppDescriber +// URI returns the service discovery namespace and is used to make +// BackendAppDescriber have the same signature as WebAppDescriber. func (d *BackendAppDescriber) URI(envName string) (string, error) { - s := serviceDiscovery{} + if err := d.initAppDescriber(envName); err != nil { + return "", err + } + appParams, err := d.appDescriber.Params() + if err != nil { + return "", fmt.Errorf("retrieve application deployment configuration: %w", err) + } + s := serviceDiscovery{ + AppName: d.app.Name, + Port: appParams[stack.LBWebAppContainerPortParamKey], + ProjectName: d.app.Project, + } return s.String(), nil } @@ -115,13 +127,13 @@ func (d *BackendAppDescriber) Describe() (HumanJSONStringer, error) { if d.enableResources { for _, env := range environments { stackResources, err := d.appDescriber.AppStackResources() - if err == nil { - resources[env.Name] = flattenResources(stackResources) - continue - } - if !IsStackNotExistsErr(err) { + if err != nil && !IsStackNotExistsErr(err) { return nil, fmt.Errorf("retrieve application resources: %w", err) } + if err != nil { + continue + } + resources[env.Name] = flattenResources(stackResources) } } @@ -167,15 +179,12 @@ func (w *backendAppDesc) HumanString() string { fmt.Fprintf(writer, " %s\t%s\n", "Type", w.Type) fmt.Fprintf(writer, color.Bold.Sprint("\nConfigurations\n\n")) writer.Flush() - fmt.Fprintf(writer, " %s\t%s\t%s\t%s\t%s\n", "Environment", "Tasks", "CPU (vCPU)", "Memory (MiB)", "Port") w.Configurations.humanString(writer) fmt.Fprintf(writer, color.Bold.Sprint("\nService Discovery\n\n")) writer.Flush() - fmt.Fprintf(writer, " %s\t%s\n", "Environment", "Namespace") w.ServiceDiscovery.humanString(writer) fmt.Fprintf(writer, color.Bold.Sprint("\nVariables\n\n")) writer.Flush() - fmt.Fprintf(writer, " %s\t%s\t%s\n", "Name", "Environment", "Value") w.Variables.humanString(writer) if len(w.Resources) != 0 { fmt.Fprintf(writer, color.Bold.Sprint("\nResources\n")) diff --git a/internal/pkg/describe/lb_web_app.go b/internal/pkg/describe/lb_web_app.go index 1467ac77aa3..bbafb566f3a 100644 --- a/internal/pkg/describe/lb_web_app.go +++ b/internal/pkg/describe/lb_web_app.go @@ -187,13 +187,13 @@ func (d *WebAppDescriber) Describe() (HumanJSONStringer, error) { if d.enableResources { for _, env := range environments { stackResources, err := d.appDescriber.AppStackResources() - if err == nil { - resources[env.Name] = flattenResources(stackResources) - continue - } - if !IsStackNotExistsErr(err) { + if err != nil && !IsStackNotExistsErr(err) { return nil, fmt.Errorf("retrieve application resources: %w", err) } + if err != nil { + continue + } + resources[env.Name] = flattenResources(stackResources) } } @@ -249,6 +249,7 @@ type EnvVars struct { type envVars []*EnvVars func (e envVars) humanString(w io.Writer) { + fmt.Fprintf(w, " %s\t%s\t%s\n", "Name", "Environment", "Value") var prevName string var prevValue string for _, variable := range e { @@ -304,6 +305,7 @@ type AppConfig struct { type configurations []*AppConfig func (c configurations) humanString(w io.Writer) { + fmt.Fprintf(w, " %s\t%s\t%s\t%s\t%s\n", "Environment", "Tasks", "CPU (vCPU)", "Memory (MiB)", "Port") for _, config := range c { fmt.Fprintf(w, " %s\t%s\t%s\t%s\t%s\n", config.Environment, config.Tasks, cpuToString(config.CPU), config.Memory, config.Port) } @@ -324,6 +326,7 @@ type ServiceDiscovery struct { type serviceDiscoveries []*ServiceDiscovery func (s serviceDiscoveries) humanString(w io.Writer) { + fmt.Fprintf(w, " %s\t%s\n", "Environment", "Namespace") for _, sd := range s { fmt.Fprintf(w, " %s\t%s\n", strings.Join(sd.Environment, ", "), sd.Namespace) } @@ -361,7 +364,6 @@ func (w *webAppDesc) HumanString() string { fmt.Fprintf(writer, " %s\t%s\n", "Type", w.Type) fmt.Fprintf(writer, color.Bold.Sprint("\nConfigurations\n\n")) writer.Flush() - fmt.Fprintf(writer, " %s\t%s\t%s\t%s\t%s\n", "Environment", "Tasks", "CPU (vCPU)", "Memory (MiB)", "Port") w.Configurations.humanString(writer) fmt.Fprintf(writer, color.Bold.Sprint("\nRoutes\n\n")) writer.Flush() @@ -371,11 +373,9 @@ func (w *webAppDesc) HumanString() string { } fmt.Fprintf(writer, color.Bold.Sprint("\nService Discovery\n\n")) writer.Flush() - fmt.Fprintf(writer, " %s\t%s\n", "Environment", "Namespace") w.ServiceDiscovery.humanString(writer) fmt.Fprintf(writer, color.Bold.Sprint("\nVariables\n\n")) writer.Flush() - fmt.Fprintf(writer, " %s\t%s\t%s\n", "Name", "Environment", "Value") w.Variables.humanString(writer) if len(w.Resources) != 0 { fmt.Fprintf(writer, color.Bold.Sprint("\nResources\n"))