diff --git a/internal/pkg/aws/ecs/task.go b/internal/pkg/aws/ecs/task.go index 22e83b51b12..f491550107d 100644 --- a/internal/pkg/aws/ecs/task.go +++ b/internal/pkg/aws/ecs/task.go @@ -114,15 +114,50 @@ func (t TaskStatus) HumanString() string { // TaskDefinition wraps up ECS TaskDefinition struct. type TaskDefinition ecs.TaskDefinition +// ContainerEnvVar holds basic info of an environment variable. +type ContainerEnvVar struct { + Name string + Container string + Value string +} + // EnvironmentVariables returns environment variables of the task definition. -func (t *TaskDefinition) EnvironmentVariables() map[string]string { - envs := make(map[string]string) - for _, env := range t.ContainerDefinitions[0].Environment { - envs[aws.StringValue(env.Name)] = aws.StringValue(env.Value) +func (t *TaskDefinition) EnvironmentVariables() []*ContainerEnvVar { + var envs []*ContainerEnvVar + for _, container := range t.ContainerDefinitions { + for _, env := range container.Environment { + envs = append(envs, &ContainerEnvVar{ + aws.StringValue(env.Name), + aws.StringValue(container.Name), + aws.StringValue(env.Value), + }) + } } return envs } +// ContainerSecret holds basic info of a secret. +type ContainerSecret struct { + Name string + Container string + ValueFrom string +} + +// Secrets returns secrets of the task definition. +func (t *TaskDefinition) Secrets() []*ContainerSecret { + var secrets []*ContainerSecret + for _, container := range t.ContainerDefinitions { + for _, secret := range container.Secrets { + secrets = append(secrets, &ContainerSecret{ + aws.StringValue(secret.Name), + aws.StringValue(container.Name), + aws.StringValue(secret.ValueFrom), + }) + } + } + return secrets +} + // TaskID parses the task ARN and returns the task ID. // For example: arn:aws:ecs:us-west-2:123456789:task/my-project-test-Cluster-9F7Y0RLP60R7/4082490ee6c245e09d2145010aa1ba8d, // arn:aws:ecs:us-west-2:123456789:task/4082490ee6c245e09d2145010aa1ba8d diff --git a/internal/pkg/aws/ecs/task_test.go b/internal/pkg/aws/ecs/task_test.go index 2675639604a..bfa3a12595f 100644 --- a/internal/pkg/aws/ecs/task_test.go +++ b/internal/pkg/aws/ecs/task_test.go @@ -243,9 +243,9 @@ func TestTaskDefinition_EnvVars(t *testing.T) { testCases := map[string]struct { inContainers []*ecs.ContainerDefinition - wantEnvVars map[string]string + wantEnvVars []*ContainerEnvVar }{ - "should return wrapped error given error": { + "should return wrapped error given error; otherwise should return list of ContainerEnvVar objects": { inContainers: []*ecs.ContainerDefinition{ { Environment: []*ecs.KeyValuePair{ @@ -258,12 +258,21 @@ func TestTaskDefinition_EnvVars(t *testing.T) { Value: aws.String("prod"), }, }, + Name: aws.String("container"), }, }, - wantEnvVars: map[string]string{ - "COPILOT_SERVICE_NAME": "my-svc", - "COPILOT_ENVIRONMENT_NAME": "prod", + wantEnvVars: []*ContainerEnvVar{ + { + Name: "COPILOT_SERVICE_NAME", + Container: "container", + Value: "my-svc", + }, + { + Name: "COPILOT_ENVIRONMENT_NAME", + Container: "container", + Value: "prod", + }, }, }, } @@ -285,3 +294,59 @@ func TestTaskDefinition_EnvVars(t *testing.T) { } } + +func TestTaskDefinition_Secrets(t *testing.T) { + testCases := map[string]struct { + inContainers []*ecs.ContainerDefinition + + wantedSecrets []*ContainerSecret + }{ + "should return secrets of the task definition as a list of ContainerSecret objects": { + inContainers: []*ecs.ContainerDefinition{ + { + Name: aws.String("container"), + Secrets: []*ecs.Secret{ + { + Name: aws.String("GITHUB_WEBHOOK_SECRET"), + ValueFrom: aws.String("GH_WEBHOOK_SECRET"), + }, + { + Name: aws.String("SOME_OTHER_SECRET"), + ValueFrom: aws.String("SHHHHHHHH"), + }, + }, + }, + }, + + wantedSecrets: []*ContainerSecret{ + { + Name: "GITHUB_WEBHOOK_SECRET", + Container: "container", + ValueFrom: "GH_WEBHOOK_SECRET", + }, + { + Name: "SOME_OTHER_SECRET", + Container: "container", + ValueFrom: "SHHHHHHHH", + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + // GIVEN + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + taskDefinition := TaskDefinition{ + ContainerDefinitions: tc.inContainers, + } + + gotSecrets := taskDefinition.Secrets() + + require.Equal(t, tc.wantedSecrets, gotSecrets) + }) + + } +} diff --git a/internal/pkg/describe/backend_service.go b/internal/pkg/describe/backend_service.go index b438c566d8c..3cf1589300b 100644 --- a/internal/pkg/describe/backend_service.go +++ b/internal/pkg/describe/backend_service.go @@ -7,7 +7,6 @@ import ( "bytes" "encoding/json" "fmt" - "sort" "text/tabwriter" "github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation/stack" @@ -99,7 +98,8 @@ func (d *BackendServiceDescriber) Describe() (HumanJSONStringer, error) { var configs []*ServiceConfig var services []*ServiceDiscovery - var envVars []*EnvVars + var envVars []*envVar + var secrets []*secret for _, env := range environments { err := d.initServiceDescriber(env) if err != nil { @@ -130,9 +130,12 @@ func (d *BackendServiceDescriber) Describe() (HumanJSONStringer, error) { return nil, fmt.Errorf("retrieve environment variables: %w", err) } envVars = append(envVars, flattenEnvVars(env, backendSvcEnvVars)...) + webSvcSecrets, err := d.svcDescriber[env].Secrets() + if err != nil { + return nil, fmt.Errorf("retrieve secrets: %w", err) + } + secrets = append(secrets, flattenSecrets(env, webSvcSecrets)...) } - 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 { @@ -156,6 +159,7 @@ func (d *BackendServiceDescriber) Describe() (HumanJSONStringer, error) { Configurations: configs, ServiceDiscovery: services, Variables: envVars, + Secrets: secrets, Resources: resources, }, nil } @@ -168,6 +172,7 @@ type backendSvcDesc struct { Configurations configurations `json:"configurations"` ServiceDiscovery serviceDiscoveries `json:"serviceDiscovery"` Variables envVars `json:"variables"` + Secrets secrets `json:"secrets,omitempty"` Resources cfnResources `json:"resources,omitempty"` } @@ -198,6 +203,11 @@ func (w *backendSvcDesc) HumanString() string { fmt.Fprint(writer, color.Bold.Sprint("\nVariables\n\n")) writer.Flush() w.Variables.humanString(writer) + if len(w.Secrets) != 0 { + fmt.Fprint(writer, color.Bold.Sprint("\nSecrets\n\n")) + writer.Flush() + w.Secrets.humanString(writer) + } if len(w.Resources) != 0 { fmt.Fprint(writer, color.Bold.Sprint("\nResources\n")) writer.Flush() diff --git a/internal/pkg/describe/backend_service_test.go b/internal/pkg/describe/backend_service_test.go index c6624eeb3b6..ac23de198ed 100644 --- a/internal/pkg/describe/backend_service_test.go +++ b/internal/pkg/describe/backend_service_test.go @@ -8,6 +8,8 @@ import ( "fmt" "testing" + "github.com/aws/copilot-cli/internal/pkg/aws/ecs" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation/stack" @@ -70,6 +72,29 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { }, wantedError: fmt.Errorf("retrieve environment variables: some error"), }, + "return error if fail to retrieve secrets": { + setupMocks: func(m backendSvcDescriberMocks) { + gomock.InOrder( + m.storeSvc.EXPECT().ListEnvironmentsDeployedTo(testApp, testSvc).Return([]string{testEnv}, nil), + + m.svcDescriber.EXPECT().Params().Return(map[string]string{ + stack.LBWebServiceContainerPortParamKey: "80", + stack.WorkloadTaskCountParamKey: "1", + stack.WorkloadTaskCPUParamKey: "256", + stack.WorkloadTaskMemoryParamKey: "512", + }, nil), + m.svcDescriber.EXPECT().EnvVars().Return([]*ecs.ContainerEnvVar{ + { + Name: "COPILOT_ENVIRONMENT_NAME", + Container: "container", + Value: "prod", + }, + }, nil), + m.svcDescriber.EXPECT().Secrets().Return(nil, mockErr), + ) + }, + wantedError: fmt.Errorf("retrieve secrets: some error"), + }, "success": { shouldOutputResources: true, setupMocks: func(m backendSvcDescriberMocks) { @@ -82,33 +107,55 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { stack.WorkloadTaskCPUParamKey: "256", stack.WorkloadTaskMemoryParamKey: "512", }, nil), - m.svcDescriber.EXPECT().EnvVars().Return( - map[string]string{ - "COPILOT_ENVIRONMENT_NAME": testEnv, - }, nil), - + m.svcDescriber.EXPECT().EnvVars().Return([]*ecs.ContainerEnvVar{ + { + Name: "COPILOT_ENVIRONMENT_NAME", + Container: "container", + Value: testEnv, + }, + }, nil), + m.svcDescriber.EXPECT().Secrets().Return([]*ecs.ContainerSecret{ + { + Name: "GITHUB_WEBHOOK_SECRET", + Container: "container", + ValueFrom: "GH_WEBHOOK_SECRET", + }, + }, nil), m.svcDescriber.EXPECT().Params().Return(map[string]string{ stack.LBWebServiceContainerPortParamKey: "5000", stack.WorkloadTaskCountParamKey: "2", stack.WorkloadTaskCPUParamKey: "512", stack.WorkloadTaskMemoryParamKey: "1024", }, nil), - m.svcDescriber.EXPECT().EnvVars().Return( - map[string]string{ - "COPILOT_ENVIRONMENT_NAME": prodEnv, - }, nil), - + m.svcDescriber.EXPECT().EnvVars().Return([]*ecs.ContainerEnvVar{ + { + Name: "COPILOT_ENVIRONMENT_NAME", + Container: "container", + Value: prodEnv, + }, + }, nil), + m.svcDescriber.EXPECT().Secrets().Return([]*ecs.ContainerSecret{ + { + Name: "SOME_OTHER_SECRET", + Container: "container", + ValueFrom: "SHHHHHHHH", + }, + }, nil), m.svcDescriber.EXPECT().Params().Return(map[string]string{ stack.LBWebServiceContainerPortParamKey: "-1", stack.WorkloadTaskCountParamKey: "2", stack.WorkloadTaskCPUParamKey: "512", stack.WorkloadTaskMemoryParamKey: "1024", }, nil), - m.svcDescriber.EXPECT().EnvVars().Return( - map[string]string{ - "COPILOT_ENVIRONMENT_NAME": mockEnv, - }, nil), - + m.svcDescriber.EXPECT().EnvVars().Return([]*ecs.ContainerEnvVar{ + { + Name: "COPILOT_ENVIRONMENT_NAME", + Container: "container", + Value: mockEnv, + }, + }, nil), + m.svcDescriber.EXPECT().Secrets().Return( + nil, nil), m.svcDescriber.EXPECT().ServiceStackResources().Return([]*cloudformation.StackResource{ { ResourceType: aws.String("AWS::EC2::SecurityGroupIngress"), @@ -162,21 +209,38 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { Namespace: "jobs.phonetool.local:5000", }, }, - Variables: []*EnvVars{ + Variables: []*envVar{ { - Environment: "mockEnv", + Container: "container", + Environment: "test", Name: "COPILOT_ENVIRONMENT_NAME", - Value: "mockEnv", + Value: "test", }, { + Container: "container", Environment: "prod", Name: "COPILOT_ENVIRONMENT_NAME", Value: "prod", }, { - Environment: "test", + Container: "container", + Environment: "mockEnv", Name: "COPILOT_ENVIRONMENT_NAME", - Value: "test", + Value: "mockEnv", + }, + }, + Secrets: []*secret{ + { + Name: "GITHUB_WEBHOOK_SECRET", + Container: "container", + Environment: "test", + ValueFrom: "GH_WEBHOOK_SECRET", + }, + { + Name: "SOME_OTHER_SECRET", + Container: "container", + Environment: "prod", + ValueFrom: "SHHHHHHHH", }, }, Resources: map[string][]*CfnResource{ @@ -259,19 +323,29 @@ func TestBackendSvcDesc_String(t *testing.T) { 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-svc.my-app.local:5000 Variables - Name Environment Value - COPILOT_ENVIRONMENT_NAME prod prod - - test test + Name Container Environment Value + ---- --------- ----------- ----- + COPILOT_ENVIRONMENT_NAME container prod prod + " " test test + +Secrets + + Name Container Environment Value From + ---- --------- ----------- ---------- + GITHUB_WEBHOOK_SECRET container test parameter/GH_WEBHOOK_SECRET + SOME_OTHER_SECRET " prod parameter/SHHHHH Resources @@ -281,7 +355,7 @@ Resources prod AWS::EC2::SecurityGroupIngress ContainerSecurityGroupIngressFromPublicALB `, - wantedJSONString: "{\"service\":\"my-svc\",\"type\":\"Backend Service\",\"application\":\"my-app\",\"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-svc.my-app.local:5000\"}],\"variables\":[{\"environment\":\"prod\",\"name\":\"COPILOT_ENVIRONMENT_NAME\",\"value\":\"prod\"},{\"environment\":\"test\",\"name\":\"COPILOT_ENVIRONMENT_NAME\",\"value\":\"test\"}],\"resources\":{\"prod\":[{\"type\":\"AWS::EC2::SecurityGroupIngress\",\"physicalID\":\"ContainerSecurityGroupIngressFromPublicALB\"}],\"test\":[{\"type\":\"AWS::EC2::SecurityGroup\",\"physicalID\":\"sg-0758ed6b233743530\"}]}}\n", + wantedJSONString: "{\"service\":\"my-svc\",\"type\":\"Backend Service\",\"application\":\"my-app\",\"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-svc.my-app.local:5000\"}],\"variables\":[{\"environment\":\"prod\",\"container\":\"container\",\"name\":\"COPILOT_ENVIRONMENT_NAME\",\"value\":\"prod\"},{\"environment\":\"test\",\"container\":\"container\",\"name\":\"COPILOT_ENVIRONMENT_NAME\",\"value\":\"test\"}],\"secrets\":[{\"name\":\"GITHUB_WEBHOOK_SECRET\",\"container\":\"container\",\"environment\":\"test\",\"valueFrom\":\"GH_WEBHOOK_SECRET\"},{\"name\":\"SOME_OTHER_SECRET\",\"container\":\"container\",\"environment\":\"prod\",\"valueFrom\":\"SHHHHH\"}],\"resources\":{\"prod\":[{\"type\":\"AWS::EC2::SecurityGroupIngress\",\"physicalID\":\"ContainerSecurityGroupIngressFromPublicALB\"}],\"test\":[{\"type\":\"AWS::EC2::SecurityGroup\",\"physicalID\":\"sg-0758ed6b233743530\"}]}}\n", }, } @@ -303,18 +377,34 @@ Resources Tasks: "3", }, } - envVars := []*EnvVars{ + envVars := []*envVar{ { + Container: "container", Environment: "prod", Name: "COPILOT_ENVIRONMENT_NAME", Value: "prod", }, { + Container: "container", Environment: "test", Name: "COPILOT_ENVIRONMENT_NAME", Value: "test", }, } + secrets := []*secret{ + { + Name: "GITHUB_WEBHOOK_SECRET", + Container: "container", + Environment: "test", + ValueFrom: "GH_WEBHOOK_SECRET", + }, + { + Name: "SOME_OTHER_SECRET", + Container: "container", + Environment: "prod", + ValueFrom: "SHHHHH", + }, + } sds := []*ServiceDiscovery{ { Environment: []string{"test", "prod"}, @@ -341,6 +431,7 @@ Resources Configurations: config, App: "my-app", Variables: envVars, + Secrets: secrets, ServiceDiscovery: sds, Resources: resources, } diff --git a/internal/pkg/describe/describe.go b/internal/pkg/describe/describe.go index de8cfd69a51..0118fa184ec 100644 --- a/internal/pkg/describe/describe.go +++ b/internal/pkg/describe/describe.go @@ -7,6 +7,8 @@ import ( "fmt" "io" + "github.com/aws/copilot-cli/internal/pkg/aws/ecs" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/dustin/go-humanize" @@ -50,16 +52,30 @@ func flattenResources(stackResources []*cloudformation.StackResource) []*CfnReso return resources } -func flattenEnvVars(envName string, m map[string]string) []*EnvVars { - var envVarList []*EnvVars - for k, v := range m { - envVarList = append(envVarList, &EnvVars{ +func flattenEnvVars(envName string, envVars []*ecs.ContainerEnvVar) []*envVar { + var out []*envVar + for _, v := range envVars { + out = append(out, &envVar{ + Name: v.Name, + Container: v.Container, + Environment: envName, + Value: v.Value, + }) + } + return out +} + +func flattenSecrets(envName string, secrets []*ecs.ContainerSecret) []*secret { + var out []*secret + for _, s := range secrets { + out = append(out, &secret{ + Name: s.Name, + Container: s.Container, Environment: envName, - Name: k, - Value: v, + ValueFrom: s.ValueFrom, }) } - return envVarList + return out } // HumanString returns the stringified CfnResource struct with human readable format. diff --git a/internal/pkg/describe/lb_web_service.go b/internal/pkg/describe/lb_web_service.go index 2f346367b3b..8005d39b11f 100644 --- a/internal/pkg/describe/lb_web_service.go +++ b/internal/pkg/describe/lb_web_service.go @@ -14,6 +14,10 @@ import ( "strings" "text/tabwriter" + "github.com/aws/copilot-cli/internal/pkg/aws/ecs" + + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation/stack" @@ -24,6 +28,9 @@ import ( const ( envOutputPublicLoadBalancerDNSName = "PublicLoadBalancerDNSName" envOutputSubdomain = "EnvironmentSubdomain" + + // Symbol used while displaying values in human format. + dittoSymbol = ` "` ) // WebServiceURI represents the unique identifier to access a web service. @@ -63,7 +70,8 @@ func (s *serviceDiscovery) String() string { type svcDescriber interface { Params() (map[string]string, error) EnvOutputs() (map[string]string, error) - EnvVars() (map[string]string, error) + EnvVars() ([]*ecs.ContainerEnvVar, error) + Secrets() ([]*ecs.ContainerSecret, error) ServiceStackResources() ([]*cloudformation.StackResource, error) } @@ -126,7 +134,8 @@ func (d *WebServiceDescriber) Describe() (HumanJSONStringer, error) { var routes []*WebServiceRoute var configs []*ServiceConfig var serviceDiscoveries []*ServiceDiscovery - var envVars []*EnvVars + var envVars []*envVar + var secrets []*secret for _, env := range environments { err := d.initServiceDescriber(env) if err != nil { @@ -157,10 +166,12 @@ func (d *WebServiceDescriber) Describe() (HumanJSONStringer, error) { return nil, fmt.Errorf("retrieve environment variables: %w", err) } envVars = append(envVars, flattenEnvVars(env, webSvcEnvVars)...) + webSvcSecrets, err := d.svcDescriber[env].Secrets() + if err != nil { + return nil, fmt.Errorf("retrieve secrets: %w", err) + } + secrets = append(secrets, flattenSecrets(env, webSvcSecrets)...) } - 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 { @@ -184,6 +195,7 @@ func (d *WebServiceDescriber) Describe() (HumanJSONStringer, error) { Routes: routes, ServiceDiscovery: serviceDiscoveries, Variables: envVars, + Secrets: secrets, Resources: resources, }, nil } @@ -219,39 +231,100 @@ func (d *WebServiceDescriber) URI(envName string) (string, error) { return uri.String(), nil } -// EnvVars contains serialized environment variables for a service. -type EnvVars struct { +// envVar contains serialized environment variables for a service. +type envVar struct { Environment string `json:"environment"` + Container string `json:"container"` Name string `json:"name"` Value string `json:"value"` } -type envVars []*EnvVars +type envVars []*envVar 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 { - // 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) - } + headers := []string{"Name", "Container", "Environment", "Value"} + fmt.Fprintf(w, " %s\n", strings.Join(headers, "\t")) + fmt.Fprintf(w, " %s\n", strings.Join(underline(headers), "\t")) + sort.SliceStable(e, func(i, j int) bool { return e[i].Environment < e[j].Environment }) + sort.SliceStable(e, func(i, j int) bool { return e[i].Container < e[j].Container }) + sort.SliceStable(e, func(i, j int) bool { return e[i].Name < e[j].Name }) + if len(e) > 0 { + fmt.Fprintf(w, " %s\n", strings.Join([]string{e[0].Name, e[0].Container, e[0].Environment, e[0].Value}, "\t")) + } + for prev, cur := 0, 1; cur < len(e); prev, cur = prev+1, cur+1 { + cols := []string{e[cur].Name, e[cur].Container, e[cur].Environment, e[cur].Value} + if e[prev].Name == e[cur].Name { + cols[0] = dittoSymbol + } + if e[prev].Container == e[cur].Container { + cols[1] = dittoSymbol + } + if e[prev].Environment == e[cur].Environment { + cols[2] = dittoSymbol + } + if e[prev].Value == e[cur].Value { + cols[3] = dittoSymbol } - prevName = variable.Name - prevValue = variable.Value + fmt.Fprintf(w, " %s\n", strings.Join(cols, "\t")) } } +type secret struct { + Name string `json:"name"` + Container string `json:"container"` + Environment string `json:"environment"` + ValueFrom string `json:"valueFrom"` +} + +type secrets []*secret + +func (s secrets) humanString(w io.Writer) { + headers := []string{"Name", "Container", "Environment", "Value From"} + fmt.Fprintf(w, " %s\n", strings.Join(headers, "\t")) + fmt.Fprintf(w, " %s\n", strings.Join(underline(headers), "\t")) + sort.SliceStable(s, func(i, j int) bool { return s[i].Environment < s[j].Environment }) + sort.SliceStable(s, func(i, j int) bool { return s[i].Container < s[j].Container }) + sort.SliceStable(s, func(i, j int) bool { return s[i].Name < s[j].Name }) + if len(s) > 0 { + valueFrom := s[0].ValueFrom + if _, err := arn.Parse(s[0].ValueFrom); err != nil { + // If the valueFrom is not an ARN, preface it with "parameter/" + valueFrom = fmt.Sprintf("parameter/%s", s[0].ValueFrom) + } + fmt.Fprintf(w, " %s\n", strings.Join([]string{s[0].Name, s[0].Container, s[0].Environment, valueFrom}, "\t")) + } + for prev, cur := 0, 1; cur < len(s); prev, cur = prev+1, cur+1 { + valueFrom := s[cur].ValueFrom + if _, err := arn.Parse(s[cur].ValueFrom); err != nil { + // If the valueFrom is not an ARN, preface it with "parameter/" + valueFrom = fmt.Sprintf("parameter/%s", s[cur].ValueFrom) + } + cols := []string{s[cur].Name, s[cur].Container, s[cur].Environment, valueFrom} + if s[prev].Name == s[cur].Name { + cols[0] = dittoSymbol + } + if s[prev].Container == s[cur].Container { + cols[1] = dittoSymbol + } + if s[prev].Environment == s[cur].Environment { + cols[2] = dittoSymbol + } + if s[prev].ValueFrom == s[cur].ValueFrom { + cols[3] = dittoSymbol + } + fmt.Fprintf(w, " %s\n", strings.Join(cols, "\t")) + } +} + +func underline(headings []string) []string { + var lines []string + for _, heading := range headings { + line := strings.Repeat("-", len(heading)) + lines = append(lines, line) + } + return lines +} + // WebServiceRoute contains serialized route parameters for a web service. type WebServiceRoute struct { Environment string `json:"environment"` @@ -267,7 +340,9 @@ type ServiceDiscovery struct { type serviceDiscoveries []*ServiceDiscovery func (s serviceDiscoveries) humanString(w io.Writer) { - fmt.Fprintf(w, " %s\t%s\n", "Environment", "Namespace") + headers := []string{"Environment", "Namespace"} + fmt.Fprintf(w, " %s\n", strings.Join(headers, "\t")) + fmt.Fprintf(w, " %s\n", strings.Join(underline(headers), "\t")) for _, sd := range s { fmt.Fprintf(w, " %s\t%s\n", strings.Join(sd.Environment, ", "), sd.Namespace) } @@ -282,6 +357,7 @@ type webSvcDesc struct { Routes []*WebServiceRoute `json:"routes"` ServiceDiscovery serviceDiscoveries `json:"serviceDiscovery"` Variables envVars `json:"variables"` + Secrets secrets `json:"secrets,omitempty"` Resources cfnResources `json:"resources,omitempty"` } @@ -308,7 +384,9 @@ func (w *webSvcDesc) HumanString() string { w.Configurations.humanString(writer) fmt.Fprint(writer, color.Bold.Sprint("\nRoutes\n\n")) writer.Flush() - fmt.Fprintf(writer, " %s\t%s\n", "Environment", "URL") + headers := []string{"Environment", "URL"} + fmt.Fprintf(writer, " %s\n", strings.Join(headers, "\t")) + fmt.Fprintf(writer, " %s\n", strings.Join(underline(headers), "\t")) for _, route := range w.Routes { fmt.Fprintf(writer, " %s\t%s\n", route.Environment, route.URL) } @@ -318,6 +396,11 @@ func (w *webSvcDesc) HumanString() string { fmt.Fprint(writer, color.Bold.Sprint("\nVariables\n\n")) writer.Flush() w.Variables.humanString(writer) + if len(w.Secrets) != 0 { + fmt.Fprint(writer, color.Bold.Sprint("\nSecrets\n\n")) + writer.Flush() + w.Secrets.humanString(writer) + } if len(w.Resources) != 0 { fmt.Fprint(writer, color.Bold.Sprint("\nResources\n")) writer.Flush() diff --git a/internal/pkg/describe/lb_web_service_test.go b/internal/pkg/describe/lb_web_service_test.go index 0dd16206cd8..65ba5df71d6 100644 --- a/internal/pkg/describe/lb_web_service_test.go +++ b/internal/pkg/describe/lb_web_service_test.go @@ -8,6 +8,8 @@ import ( "fmt" "testing" + "github.com/aws/copilot-cli/internal/pkg/aws/ecs" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation/stack" @@ -219,6 +221,52 @@ func TestWebServiceDescriber_Describe(t *testing.T) { }, wantedError: fmt.Errorf("retrieve environment variables: some error"), }, + "return error if fail to retrieve environment variables": { + setupMocks: func(m webSvcDescriberMocks) { + gomock.InOrder( + m.storeSvc.EXPECT().ListEnvironmentsDeployedTo(testApp, testSvc).Return([]string{testEnv}, nil), + m.svcDescriber.EXPECT().EnvOutputs().Return(map[string]string{ + envOutputPublicLoadBalancerDNSName: testEnvLBDNSName, + }, nil), + m.svcDescriber.EXPECT().Params().Return(map[string]string{ + stack.LBWebServiceContainerPortParamKey: "80", + stack.WorkloadTaskCountParamKey: "1", + stack.WorkloadTaskCPUParamKey: "256", + stack.WorkloadTaskMemoryParamKey: "512", + stack.LBWebServiceRulePathParamKey: testSvcPath, + }, nil), + m.svcDescriber.EXPECT().EnvVars().Return(nil, mockErr), + ) + }, + wantedError: fmt.Errorf("retrieve environment variables: some error"), + }, + "return error if fail to retrieve secrets": { + setupMocks: func(m webSvcDescriberMocks) { + gomock.InOrder( + m.storeSvc.EXPECT().ListEnvironmentsDeployedTo(testApp, testSvc).Return([]string{testEnv}, nil), + m.svcDescriber.EXPECT().EnvOutputs().Return(map[string]string{ + envOutputPublicLoadBalancerDNSName: testEnvLBDNSName, + }, nil), + m.svcDescriber.EXPECT().Params().Return(map[string]string{ + stack.LBWebServiceContainerPortParamKey: "80", + stack.WorkloadTaskCountParamKey: "1", + stack.WorkloadTaskCPUParamKey: "256", + stack.WorkloadTaskMemoryParamKey: "512", + stack.LBWebServiceRulePathParamKey: testSvcPath, + }, nil), + m.svcDescriber.EXPECT().EnvVars().Return([]*ecs.ContainerEnvVar{ + { + Name: "COPILOT_ENVIRONMENT_NAME", + Container: "container", + Value: "prod", + }, + }, nil), + + m.svcDescriber.EXPECT().Secrets().Return(nil, mockErr), + ) + }, + wantedError: fmt.Errorf("retrieve secrets: some error"), + }, "return error if fail to retrieve service resources": { shouldOutputResources: true, setupMocks: func(m webSvcDescriberMocks) { @@ -234,10 +282,25 @@ func TestWebServiceDescriber_Describe(t *testing.T) { stack.WorkloadTaskCPUParamKey: "256", stack.WorkloadTaskMemoryParamKey: "512", }, nil), - m.svcDescriber.EXPECT().EnvVars().Return( - map[string]string{ - "COPILOT_ENVIRONMENT_NAME": testEnv, - }, nil), + m.svcDescriber.EXPECT().EnvVars().Return([]*ecs.ContainerEnvVar{ + { + Name: "COPILOT_ENVIRONMENT_NAME", + Container: "container", + Value: "test", + }, + }, nil), + m.svcDescriber.EXPECT().Secrets().Return([]*ecs.ContainerSecret{ + { + Name: "GITHUB_WEBHOOK_SECRET", + Container: "container", + ValueFrom: "GH_WEBHOOK_SECRET", + }, + { + Name: "SOME_OTHER_SECRET", + Container: "container", + ValueFrom: "SHHHHHHHH", + }, + }, nil), m.svcDescriber.EXPECT().ServiceStackResources().Return(nil, mockErr), ) }, @@ -259,11 +322,20 @@ func TestWebServiceDescriber_Describe(t *testing.T) { stack.WorkloadTaskCPUParamKey: "256", stack.WorkloadTaskMemoryParamKey: "512", }, nil), - m.svcDescriber.EXPECT().EnvVars().Return( - map[string]string{ - "COPILOT_ENVIRONMENT_NAME": testEnv, - }, nil), - + m.svcDescriber.EXPECT().EnvVars().Return([]*ecs.ContainerEnvVar{ + { + Name: "COPILOT_ENVIRONMENT_NAME", + Container: "container1", + Value: testEnv, + }, + }, nil), + m.svcDescriber.EXPECT().Secrets().Return([]*ecs.ContainerSecret{ + { + Name: "GITHUB_WEBHOOK_SECRET", + Container: "container", + ValueFrom: "GH_WEBHOOK_SECRET", + }, + }, nil), m.svcDescriber.EXPECT().EnvOutputs().Return(map[string]string{ envOutputPublicLoadBalancerDNSName: prodEnvLBDNSName, }, nil), @@ -274,11 +346,20 @@ func TestWebServiceDescriber_Describe(t *testing.T) { stack.WorkloadTaskCPUParamKey: "512", stack.WorkloadTaskMemoryParamKey: "1024", }, nil), - m.svcDescriber.EXPECT().EnvVars().Return( - map[string]string{ - "COPILOT_ENVIRONMENT_NAME": prodEnv, - }, nil), - + m.svcDescriber.EXPECT().EnvVars().Return([]*ecs.ContainerEnvVar{ + { + Name: "COPILOT_ENVIRONMENT_NAME", + Container: "container2", + Value: prodEnv, + }, + }, nil), + m.svcDescriber.EXPECT().Secrets().Return([]*ecs.ContainerSecret{ + { + Name: "SOME_OTHER_SECRET", + Container: "container", + ValueFrom: "SHHHHHHHH", + }, + }, nil), m.svcDescriber.EXPECT().ServiceStackResources().Return([]*cloudformation.StackResource{ { ResourceType: aws.String("AWS::EC2::SecurityGroupIngress"), @@ -329,16 +410,32 @@ func TestWebServiceDescriber_Describe(t *testing.T) { Namespace: "jobs.phonetool.local:5000", }, }, - Variables: []*EnvVars{ + Variables: []*envVar{ + { + Environment: "test", + Container: "container1", + Name: "COPILOT_ENVIRONMENT_NAME", + Value: "test", + }, { Environment: "prod", + Container: "container2", Name: "COPILOT_ENVIRONMENT_NAME", Value: "prod", }, + }, + Secrets: []*secret{ { + Name: "GITHUB_WEBHOOK_SECRET", + Container: "container", Environment: "test", - Name: "COPILOT_ENVIRONMENT_NAME", - Value: "test", + ValueFrom: "GH_WEBHOOK_SECRET", + }, + { + Name: "SOME_OTHER_SECRET", + Container: "container", + Environment: "prod", + ValueFrom: "SHHHHHHHH", }, }, Resources: map[string][]*CfnResource{ @@ -404,7 +501,7 @@ func TestWebServiceDesc_String(t *testing.T) { wantedHumanString string wantedJSONString string }{ - "correct output": { + "correct output including env vars and secrets sorted (name, container, env) and double-quotes for ditto": { wantedHumanString: `About Application my-app @@ -414,25 +511,37 @@ func TestWebServiceDesc_String(t *testing.T) { 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-svc.my-app.local:5000 Variables - Name Environment Value - COPILOT_ENVIRONMENT_NAME prod prod - - test test + Name Container Environment Value + ---- --------- ----------- ----- + COPILOT_ENVIRONMENT_NAME containerA test test + " containerB prod prod + DIFFERENT_ENV_VAR " " " + +Secrets + + Name Container Environment Value From + ---- --------- ----------- ---------- + GITHUB_WEBHOOK_SECRET containerA test parameter/GH_WEBHOOK_SECRET + SOME_OTHER_SECRET containerB prod parameter/SHHHHH Resources @@ -442,7 +551,7 @@ Resources prod AWS::EC2::SecurityGroupIngress ContainerSecurityGroupIngressFromPublicALB `, - wantedJSONString: "{\"service\":\"my-svc\",\"type\":\"Load Balanced Web Service\",\"application\":\"my-app\",\"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-svc.my-app.local:5000\"}],\"variables\":[{\"environment\":\"prod\",\"name\":\"COPILOT_ENVIRONMENT_NAME\",\"value\":\"prod\"},{\"environment\":\"test\",\"name\":\"COPILOT_ENVIRONMENT_NAME\",\"value\":\"test\"}],\"resources\":{\"prod\":[{\"type\":\"AWS::EC2::SecurityGroupIngress\",\"physicalID\":\"ContainerSecurityGroupIngressFromPublicALB\"}],\"test\":[{\"type\":\"AWS::EC2::SecurityGroup\",\"physicalID\":\"sg-0758ed6b233743530\"}]}}\n", + wantedJSONString: "{\"service\":\"my-svc\",\"type\":\"Load Balanced Web Service\",\"application\":\"my-app\",\"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-svc.my-app.local:5000\"}],\"variables\":[{\"environment\":\"test\",\"container\":\"containerA\",\"name\":\"COPILOT_ENVIRONMENT_NAME\",\"value\":\"test\"},{\"environment\":\"prod\",\"container\":\"containerB\",\"name\":\"COPILOT_ENVIRONMENT_NAME\",\"value\":\"prod\"},{\"environment\":\"prod\",\"container\":\"containerB\",\"name\":\"DIFFERENT_ENV_VAR\",\"value\":\"prod\"}],\"secrets\":[{\"name\":\"GITHUB_WEBHOOK_SECRET\",\"container\":\"containerA\",\"environment\":\"test\",\"valueFrom\":\"GH_WEBHOOK_SECRET\"},{\"name\":\"SOME_OTHER_SECRET\",\"container\":\"containerB\",\"environment\":\"prod\",\"valueFrom\":\"SHHHHH\"}],\"resources\":{\"prod\":[{\"type\":\"AWS::EC2::SecurityGroupIngress\",\"physicalID\":\"ContainerSecurityGroupIngressFromPublicALB\"}],\"test\":[{\"type\":\"AWS::EC2::SecurityGroup\",\"physicalID\":\"sg-0758ed6b233743530\"}]}}\n", }, } @@ -464,17 +573,39 @@ Resources Tasks: "3", }, } - envVars := []*EnvVars{ + envVars := []*envVar{ { Environment: "prod", + Container: "containerB", Name: "COPILOT_ENVIRONMENT_NAME", Value: "prod", }, { Environment: "test", + Container: "containerA", Name: "COPILOT_ENVIRONMENT_NAME", Value: "test", }, + { + Environment: "prod", + Container: "containerB", + Name: "DIFFERENT_ENV_VAR", + Value: "prod", + }, + } + secrets := []*secret{ + { + Name: "GITHUB_WEBHOOK_SECRET", + Container: "containerA", + Environment: "test", + ValueFrom: "GH_WEBHOOK_SECRET", + }, + { + Name: "SOME_OTHER_SECRET", + Container: "containerB", + Environment: "prod", + ValueFrom: "SHHHHH", + }, } routes := []*WebServiceRoute{ { @@ -512,6 +643,7 @@ Resources Configurations: config, App: "my-app", Variables: envVars, + Secrets: secrets, Routes: routes, ServiceDiscovery: sds, Resources: resources, diff --git a/internal/pkg/describe/mocks/mock_lb_web_service.go b/internal/pkg/describe/mocks/mock_lb_web_service.go index e8e995b99f2..27b21bb1fbd 100644 --- a/internal/pkg/describe/mocks/mock_lb_web_service.go +++ b/internal/pkg/describe/mocks/mock_lb_web_service.go @@ -6,6 +6,7 @@ package mocks import ( cloudformation "github.com/aws/aws-sdk-go/service/cloudformation" + ecs "github.com/aws/copilot-cli/internal/pkg/aws/ecs" gomock "github.com/golang/mock/gomock" reflect "reflect" ) @@ -64,10 +65,10 @@ func (mr *MocksvcDescriberMockRecorder) EnvOutputs() *gomock.Call { } // EnvVars mocks base method -func (m *MocksvcDescriber) EnvVars() (map[string]string, error) { +func (m *MocksvcDescriber) EnvVars() ([]*ecs.ContainerEnvVar, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "EnvVars") - ret0, _ := ret[0].(map[string]string) + ret0, _ := ret[0].([]*ecs.ContainerEnvVar) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -78,6 +79,21 @@ func (mr *MocksvcDescriberMockRecorder) EnvVars() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnvVars", reflect.TypeOf((*MocksvcDescriber)(nil).EnvVars)) } +// Secrets mocks base method +func (m *MocksvcDescriber) Secrets() ([]*ecs.ContainerSecret, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Secrets") + ret0, _ := ret[0].([]*ecs.ContainerSecret) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Secrets indicates an expected call of Secrets +func (mr *MocksvcDescriberMockRecorder) Secrets() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Secrets", reflect.TypeOf((*MocksvcDescriber)(nil).Secrets)) +} + // ServiceStackResources mocks base method func (m *MocksvcDescriber) ServiceStackResources() ([]*cloudformation.StackResource, error) { m.ctrl.T.Helper() diff --git a/internal/pkg/describe/service.go b/internal/pkg/describe/service.go index fa356c9e8c2..63f211a21e2 100644 --- a/internal/pkg/describe/service.go +++ b/internal/pkg/describe/service.go @@ -6,6 +6,7 @@ package describe import ( "fmt" "io" + "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudformation" @@ -57,7 +58,9 @@ type ServiceConfig struct { type configurations []*ServiceConfig 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") + headers := []string{"Environment", "Tasks", "CPU (vCPU)", "Memory (MiB)", "Port"} + fmt.Fprintf(w, " %s\n", strings.Join(headers, "\t")) + fmt.Fprintf(w, " %s\n", strings.Join(underline(headers), "\t")) 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) } @@ -103,15 +106,23 @@ func NewServiceDescriber(opt NewServiceConfig) (*ServiceDescriber, error) { } // EnvVars returns the environment variables of the task definition. -func (d *ServiceDescriber) EnvVars() (map[string]string, error) { +func (d *ServiceDescriber) EnvVars() ([]*ecs.ContainerEnvVar, error) { taskDefName := fmt.Sprintf("%s-%s-%s", d.app, d.env, d.service) taskDefinition, err := d.ecsClient.TaskDefinition(taskDefName) if err != nil { return nil, err } - envVars := taskDefinition.EnvironmentVariables() + return taskDefinition.EnvironmentVariables(), nil +} - return envVars, nil +// Secrets returns the secrets of the task definition. +func (d *ServiceDescriber) Secrets() ([]*ecs.ContainerSecret, error) { + taskDefName := fmt.Sprintf("%s-%s-%s", d.app, d.env, d.service) + taskDefinition, err := d.ecsClient.TaskDefinition(taskDefName) + if err != nil { + return nil, err + } + return taskDefinition.Secrets(), nil } // ServiceStackResources returns the filtered service stack resources created by CloudFormation. diff --git a/internal/pkg/describe/service_test.go b/internal/pkg/describe/service_test.go index 3c8dc6cfb5b..f42f7050368 100644 --- a/internal/pkg/describe/service_test.go +++ b/internal/pkg/describe/service_test.go @@ -34,7 +34,7 @@ func TestServiceDescriber_EnvVars(t *testing.T) { testCases := map[string]struct { setupMocks func(mocks svcDescriberMocks) - wantedEnvVars map[string]string + wantedEnvVars []*ecs.ContainerEnvVar wantedError error }{ "returns error if fails to get environment variables": { @@ -62,14 +62,23 @@ func TestServiceDescriber_EnvVars(t *testing.T) { Value: aws.String("prod"), }, }, + Name: aws.String("container"), }, }, }, nil), ) }, - wantedEnvVars: map[string]string{ - "COPILOT_SERVICE_NAME": "my-svc", - "COPILOT_ENVIRONMENT_NAME": "prod", + wantedEnvVars: []*ecs.ContainerEnvVar{ + { + Name: "COPILOT_SERVICE_NAME", + Container: "container", + Value: "my-svc", + }, + { + Name: "COPILOT_ENVIRONMENT_NAME", + Container: "container", + Value: "prod", + }, }, }, } @@ -112,6 +121,102 @@ func TestServiceDescriber_EnvVars(t *testing.T) { } } +func TestServiceDescriber_Secrets(t *testing.T) { + const ( + testApp = "phonetool" + testSvc = "jobs" + testEnv = "test" + ) + testCases := map[string]struct { + setupMocks func(mocks svcDescriberMocks) + + wantedSecrets []*ecs.ContainerSecret + wantedError error + }{ + "returns error if fails to get secrets": { + setupMocks: func(m svcDescriberMocks) { + gomock.InOrder( + m.mockecsClient.EXPECT().TaskDefinition("phonetool-test-jobs").Return(nil, errors.New("some error")), + ) + }, + + wantedError: fmt.Errorf("some error"), + }, + "successfully gets secrets": { + setupMocks: func(m svcDescriberMocks) { + gomock.InOrder( + m.mockecsClient.EXPECT().TaskDefinition("phonetool-test-jobs").Return(&ecs.TaskDefinition{ + ContainerDefinitions: []*ecsapi.ContainerDefinition{ + { + Name: aws.String("container"), + Secrets: []*ecsapi.Secret{ + { + Name: aws.String("GITHUB_WEBHOOK_SECRET"), + ValueFrom: aws.String("GH_WEBHOOK_SECRET"), + }, + { + Name: aws.String("SOME_OTHER_SECRET"), + ValueFrom: aws.String("SHHHHHHHH"), + }, + }, + }, + }, + }, nil), + ) + }, + wantedSecrets: []*ecs.ContainerSecret{ + { + Name: "GITHUB_WEBHOOK_SECRET", + Container: "container", + ValueFrom: "GH_WEBHOOK_SECRET", + }, + { + Name: "SOME_OTHER_SECRET", + Container: "container", + ValueFrom: "SHHHHHHHH", + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + // GIVEN + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockecsClient := mocks.NewMockecsClient(ctrl) + mockStackDescriber := mocks.NewMockstackAndResourcesDescriber(ctrl) + mocks := svcDescriberMocks{ + mockecsClient: mockecsClient, + mockStackDescriber: mockStackDescriber, + } + + tc.setupMocks(mocks) + + d := &ServiceDescriber{ + app: testApp, + service: testSvc, + env: testEnv, + + ecsClient: mockecsClient, + stackDescriber: mockStackDescriber, + } + + // WHEN + actual, err := d.Secrets() + + // THEN + if tc.wantedError != nil { + require.EqualError(t, err, tc.wantedError.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tc.wantedSecrets, actual) + } + }) + } +} + func TestServiceDescriber_ServiceStackResources(t *testing.T) { const ( testApp = "phonetool"