diff --git a/internal/pkg/cli/svc_deploy.go b/internal/pkg/cli/svc_deploy.go index 20c12919880..e2d7c8c0b2b 100644 --- a/internal/pkg/cli/svc_deploy.go +++ b/internal/pkg/cli/svc_deploy.go @@ -422,13 +422,16 @@ func (o *deploySvcOpts) uriRecommendedActions() ([]string, error) { } network := "over the internet." - if o.svcType == manifest.BackendServiceType { + switch uri.AccessType { + case describe.URIAccessTypeInternal: + network = "from your internal network." + case describe.URIAccessTypeServiceDiscovery: network = "with service discovery." } - recs := []string{ - fmt.Sprintf("You can access your service at %s %s", color.HighlightResource(uri), network), - } - return recs, nil + + return []string{ + fmt.Sprintf("You can access your service at %s %s", color.HighlightResource(uri.URI), network), + }, nil } func (o *deploySvcOpts) publishRecommendedActions() []string { diff --git a/internal/pkg/describe/backend_service.go b/internal/pkg/describe/backend_service.go index 86e6e051b0e..c77340ae9c2 100644 --- a/internal/pkg/describe/backend_service.go +++ b/internal/pkg/describe/backend_service.go @@ -7,8 +7,11 @@ import ( "bytes" "encoding/json" "fmt" + "strings" "text/tabwriter" + "github.com/aws/copilot-cli/internal/pkg/aws/elbv2" + "github.com/aws/copilot-cli/internal/pkg/aws/sessions" "github.com/aws/copilot-cli/internal/pkg/docker/dockerengine" cfnstack "github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation/stack" @@ -33,6 +36,7 @@ type BackendServiceDescriber struct { store DeployedEnvServicesLister initECSServiceDescribers func(string) (ecsDescriber, error) initEnvDescribers func(string) (envDescriber, error) + initLBDescriber func(string) (lbDescriber, error) ecsServiceDescribers map[string]ecsDescriber envStackDescriber map[string]envDescriber } @@ -47,6 +51,17 @@ func NewBackendServiceDescriber(opt NewServiceConfig) (*BackendServiceDescriber, ecsServiceDescribers: make(map[string]ecsDescriber), envStackDescriber: make(map[string]envDescriber), } + describer.initLBDescriber = func(envName string) (lbDescriber, error) { + env, err := opt.ConfigStore.GetEnvironment(opt.App, envName) + if err != nil { + return nil, fmt.Errorf("get environment %s: %w", envName, err) + } + sess, err := sessions.ImmutableProvider().FromRole(env.ManagerRoleARN, env.Region) + if err != nil { + return nil, err + } + return elbv2.New(sess), nil + } describer.initECSServiceDescribers = func(env string) (ecsDescriber, error) { if describer, ok := describer.ecsServiceDescribers[env]; ok { return describer, nil @@ -87,6 +102,7 @@ func (d *BackendServiceDescriber) Describe() (HumanJSONStringer, error) { return nil, fmt.Errorf("list deployed environments for application %s: %w", d.app, err) } + var routes []*WebServiceRoute var configs []*ECSServiceConfig var services []*ServiceDiscovery var envVars []*containerEnvVar @@ -96,6 +112,16 @@ func (d *BackendServiceDescriber) Describe() (HumanJSONStringer, error) { if err != nil { return nil, err } + uri, err := d.URI(env) + if err != nil { + return nil, fmt.Errorf("retrieve service URI: %w", err) + } + if uri.AccessType == URIAccessTypeInternal { + routes = append(routes, &WebServiceRoute{ + Environment: env, + URL: uri.URI, + }) + } svcParams, err := svcDescr.Params() if err != nil { return nil, fmt.Errorf("get stack parameters for environment %s: %w", env, err) @@ -163,6 +189,7 @@ func (d *BackendServiceDescriber) Describe() (HumanJSONStringer, error) { Type: manifest.BackendServiceType, App: d.app, Configurations: configs, + Routes: routes, ServiceDiscovery: services, Variables: envVars, Secrets: secrets, @@ -178,6 +205,7 @@ type backendSvcDesc struct { Type string `json:"type"` App string `json:"application"` Configurations ecsConfigurations `json:"configurations"` + Routes []*WebServiceRoute `json:"routes"` ServiceDiscovery serviceDiscoveries `json:"serviceDiscovery"` Variables containerEnvVars `json:"variables"` Secrets secrets `json:"secrets,omitempty"` @@ -207,6 +235,16 @@ func (w *backendSvcDesc) HumanString() string { fmt.Fprint(writer, color.Bold.Sprint("\nConfigurations\n\n")) writer.Flush() w.Configurations.humanString(writer) + if len(w.Routes) > 0 { + fmt.Fprint(writer, color.Bold.Sprint("\nRoutes\n\n")) + writer.Flush() + 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) + } + } fmt.Fprint(writer, color.Bold.Sprint("\nService Discovery\n\n")) writer.Flush() w.ServiceDiscovery.humanString(writer) diff --git a/internal/pkg/describe/backend_service_test.go b/internal/pkg/describe/backend_service_test.go index 2ed347a891a..800c50de927 100644 --- a/internal/pkg/describe/backend_service_test.go +++ b/internal/pkg/describe/backend_service_test.go @@ -13,6 +13,7 @@ import ( cfnstack "github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation/stack" "github.com/aws/copilot-cli/internal/pkg/describe/mocks" "github.com/aws/copilot-cli/internal/pkg/describe/stack" + describeStack "github.com/aws/copilot-cli/internal/pkg/describe/stack" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" ) @@ -46,15 +47,17 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { setupMocks: func(m lbWebSvcDescriberMocks) { gomock.InOrder( m.storeSvc.EXPECT().ListEnvironmentsDeployedTo(testApp, testSvc).Return([]string{testEnv}, nil), + m.ecsDescriber.EXPECT().ServiceStackResources().Return(nil, nil), m.ecsDescriber.EXPECT().Params().Return(nil, mockErr), ) }, - wantedError: fmt.Errorf("get stack parameters for environment test: some error"), + wantedError: fmt.Errorf("retrieve service URI: get stack parameters for environment test: some error"), }, "return error if fail to retrieve svc discovery endpoint": { setupMocks: func(m lbWebSvcDescriberMocks) { gomock.InOrder( m.storeSvc.EXPECT().ListEnvironmentsDeployedTo(testApp, testSvc).Return([]string{testEnv}, nil), + m.ecsDescriber.EXPECT().ServiceStackResources().Return(nil, nil), m.ecsDescriber.EXPECT().Params().Return(map[string]string{ cfnstack.WorkloadContainerPortParamKey: "80", cfnstack.WorkloadTaskCountParamKey: "1", @@ -64,18 +67,22 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { m.envDescriber.EXPECT().ServiceDiscoveryEndpoint().Return("", errors.New("some error")), ) }, - wantedError: fmt.Errorf("some error"), + wantedError: fmt.Errorf("retrieve service URI: retrieve service discovery endpoint for environment test: some error"), }, "return error if fail to retrieve platform": { setupMocks: func(m lbWebSvcDescriberMocks) { + params := map[string]string{ + cfnstack.WorkloadContainerPortParamKey: "5000", + cfnstack.WorkloadTaskCountParamKey: "1", + cfnstack.WorkloadTaskCPUParamKey: "256", + cfnstack.WorkloadTaskMemoryParamKey: "512", + } gomock.InOrder( m.storeSvc.EXPECT().ListEnvironmentsDeployedTo(testApp, testSvc).Return([]string{testEnv}, nil), - m.ecsDescriber.EXPECT().Params().Return(map[string]string{ - cfnstack.WorkloadContainerPortParamKey: "5000", - cfnstack.WorkloadTaskCountParamKey: "1", - cfnstack.WorkloadTaskCPUParamKey: "256", - cfnstack.WorkloadTaskMemoryParamKey: "512", - }, nil), + m.ecsDescriber.EXPECT().ServiceStackResources().Return(nil, nil), + m.ecsDescriber.EXPECT().Params().Return(params, nil), + m.envDescriber.EXPECT().ServiceDiscoveryEndpoint().Return("test.phonetool.local", nil), + m.ecsDescriber.EXPECT().Params().Return(params, nil), m.envDescriber.EXPECT().ServiceDiscoveryEndpoint().Return("test.phonetool.local", nil), m.ecsDescriber.EXPECT().Platform().Return(nil, errors.New("some error")), ) @@ -84,14 +91,18 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { }, "return error if fail to retrieve environment variables": { setupMocks: func(m lbWebSvcDescriberMocks) { + params := map[string]string{ + cfnstack.WorkloadContainerPortParamKey: "5000", + cfnstack.WorkloadTaskCountParamKey: "1", + cfnstack.WorkloadTaskCPUParamKey: "256", + cfnstack.WorkloadTaskMemoryParamKey: "512", + } gomock.InOrder( m.storeSvc.EXPECT().ListEnvironmentsDeployedTo(testApp, testSvc).Return([]string{testEnv}, nil), - m.ecsDescriber.EXPECT().Params().Return(map[string]string{ - cfnstack.WorkloadContainerPortParamKey: "80", - cfnstack.WorkloadTaskCountParamKey: "1", - cfnstack.WorkloadTaskMemoryParamKey: "512", - cfnstack.WorkloadTaskCPUParamKey: "256", - }, nil), + m.ecsDescriber.EXPECT().ServiceStackResources().Return(nil, nil), + m.ecsDescriber.EXPECT().Params().Return(params, nil), + m.envDescriber.EXPECT().ServiceDiscoveryEndpoint().Return("test.phonetool.local", nil), + m.ecsDescriber.EXPECT().Params().Return(params, nil), m.envDescriber.EXPECT().ServiceDiscoveryEndpoint().Return("test.phonetool.local", nil), m.ecsDescriber.EXPECT().Platform().Return(&ecs.ContainerPlatform{ OperatingSystem: "LINUX", @@ -104,15 +115,18 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { }, "return error if fail to retrieve secrets": { setupMocks: func(m lbWebSvcDescriberMocks) { + params := map[string]string{ + cfnstack.WorkloadContainerPortParamKey: "80", + cfnstack.WorkloadTaskCountParamKey: "1", + cfnstack.WorkloadTaskCPUParamKey: "256", + cfnstack.WorkloadTaskMemoryParamKey: "512", + } gomock.InOrder( m.storeSvc.EXPECT().ListEnvironmentsDeployedTo(testApp, testSvc).Return([]string{testEnv}, nil), - - m.ecsDescriber.EXPECT().Params().Return(map[string]string{ - cfnstack.WorkloadContainerPortParamKey: "80", - cfnstack.WorkloadTaskCountParamKey: "1", - cfnstack.WorkloadTaskCPUParamKey: "256", - cfnstack.WorkloadTaskMemoryParamKey: "512", - }, nil), + m.ecsDescriber.EXPECT().ServiceStackResources().Return(nil, nil), + m.ecsDescriber.EXPECT().Params().Return(params, nil), + m.envDescriber.EXPECT().ServiceDiscoveryEndpoint().Return("test.phonetool.local", nil), + m.ecsDescriber.EXPECT().Params().Return(params, nil), m.envDescriber.EXPECT().ServiceDiscoveryEndpoint().Return("test.phonetool.local", nil), m.ecsDescriber.EXPECT().Platform().Return(&ecs.ContainerPlatform{ OperatingSystem: "LINUX", @@ -133,15 +147,30 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { "success": { shouldOutputResources: true, setupMocks: func(m lbWebSvcDescriberMocks) { + testParams := map[string]string{ + cfnstack.WorkloadContainerPortParamKey: "5000", + cfnstack.WorkloadTaskCountParamKey: "1", + cfnstack.WorkloadTaskCPUParamKey: "256", + cfnstack.WorkloadTaskMemoryParamKey: "512", + } + prodParams := map[string]string{ + cfnstack.WorkloadContainerPortParamKey: "5000", + cfnstack.WorkloadTaskCountParamKey: "2", + cfnstack.WorkloadTaskCPUParamKey: "512", + cfnstack.WorkloadTaskMemoryParamKey: "1024", + } + mockParams := map[string]string{ + cfnstack.WorkloadContainerPortParamKey: "-1", + cfnstack.WorkloadTaskCountParamKey: "2", + cfnstack.WorkloadTaskCPUParamKey: "512", + cfnstack.WorkloadTaskMemoryParamKey: "1024", + } gomock.InOrder( m.storeSvc.EXPECT().ListEnvironmentsDeployedTo(testApp, testSvc).Return([]string{testEnv, prodEnv, mockEnv}, nil), - - m.ecsDescriber.EXPECT().Params().Return(map[string]string{ - cfnstack.WorkloadContainerPortParamKey: "5000", - cfnstack.WorkloadTaskCountParamKey: "1", - cfnstack.WorkloadTaskCPUParamKey: "256", - cfnstack.WorkloadTaskMemoryParamKey: "512", - }, nil), + m.ecsDescriber.EXPECT().ServiceStackResources().Return(nil, nil), + m.ecsDescriber.EXPECT().Params().Return(testParams, nil), + m.envDescriber.EXPECT().ServiceDiscoveryEndpoint().Return("test.phonetool.local", nil), + m.ecsDescriber.EXPECT().Params().Return(testParams, nil), m.envDescriber.EXPECT().ServiceDiscoveryEndpoint().Return("test.phonetool.local", nil), m.ecsDescriber.EXPECT().Platform().Return(&ecs.ContainerPlatform{ OperatingSystem: "LINUX", @@ -161,12 +190,10 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { ValueFrom: "GH_WEBHOOK_SECRET", }, }, nil), - m.ecsDescriber.EXPECT().Params().Return(map[string]string{ - cfnstack.WorkloadContainerPortParamKey: "5000", - cfnstack.WorkloadTaskCountParamKey: "2", - cfnstack.WorkloadTaskCPUParamKey: "512", - cfnstack.WorkloadTaskMemoryParamKey: "1024", - }, nil), + m.ecsDescriber.EXPECT().ServiceStackResources().Return(nil, nil), + m.ecsDescriber.EXPECT().Params().Return(prodParams, nil), + m.envDescriber.EXPECT().ServiceDiscoveryEndpoint().Return("prod.phonetool.local", nil), + m.ecsDescriber.EXPECT().Params().Return(prodParams, nil), m.envDescriber.EXPECT().ServiceDiscoveryEndpoint().Return("prod.phonetool.local", nil), m.ecsDescriber.EXPECT().Platform().Return(&ecs.ContainerPlatform{ OperatingSystem: "LINUX", @@ -186,12 +213,9 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { ValueFrom: "SHHHHHHHH", }, }, nil), - m.ecsDescriber.EXPECT().Params().Return(map[string]string{ - cfnstack.WorkloadContainerPortParamKey: "-1", - cfnstack.WorkloadTaskCountParamKey: "2", - cfnstack.WorkloadTaskCPUParamKey: "512", - cfnstack.WorkloadTaskMemoryParamKey: "1024", - }, nil), + m.ecsDescriber.EXPECT().ServiceStackResources().Return(nil, nil), + m.ecsDescriber.EXPECT().Params().Return(mockParams, nil), + m.ecsDescriber.EXPECT().Params().Return(mockParams, nil), m.ecsDescriber.EXPECT().Platform().Return(&ecs.ContainerPlatform{ OperatingSystem: "LINUX", Architecture: "X86_64", @@ -334,6 +358,110 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { environments: []string{"test", "prod", "mockEnv"}, }, }, + "internal alb success http": { + shouldOutputResources: true, + setupMocks: func(m lbWebSvcDescriberMocks) { + params := map[string]string{ + cfnstack.WorkloadContainerPortParamKey: "5000", + cfnstack.WorkloadTaskCountParamKey: "1", + cfnstack.WorkloadTaskCPUParamKey: "256", + cfnstack.WorkloadTaskMemoryParamKey: "512", + cfnstack.WorkloadRulePathParamKey: "mySvc", + } + gomock.InOrder( + m.storeSvc.EXPECT().ListEnvironmentsDeployedTo(testApp, testSvc).Return([]string{testEnv}, nil), + m.ecsDescriber.EXPECT().ServiceStackResources().Return([]*describeStack.Resource{ + { + LogicalID: svcStackResourceALBTargetGroupLogicalID, + }, + }, nil), + m.ecsDescriber.EXPECT().Params().Return(params, nil), + m.envDescriber.EXPECT().Outputs().Return(map[string]string{ + envOutputInternalLoadBalancerDNSName: "us-west-1.elb.amazonaws.internal", + }, nil), + m.ecsDescriber.EXPECT().Params().Return(params, nil), + m.envDescriber.EXPECT().ServiceDiscoveryEndpoint().Return("test.phonetool.local", nil), + m.ecsDescriber.EXPECT().Platform().Return(&ecs.ContainerPlatform{ + OperatingSystem: "LINUX", + Architecture: "X86_64", + }, nil), + m.ecsDescriber.EXPECT().EnvVars().Return([]*ecs.ContainerEnvVar{ + { + Name: "COPILOT_ENVIRONMENT_NAME", + Container: "container", + Value: testEnv, + }, + }, nil), + m.ecsDescriber.EXPECT().Secrets().Return([]*ecs.ContainerSecret{ + { + Name: "GITHUB_WEBHOOK_SECRET", + Container: "container", + ValueFrom: "GH_WEBHOOK_SECRET", + }, + }, nil), + m.ecsDescriber.EXPECT().ServiceStackResources().Return([]*describeStack.Resource{ + { + LogicalID: svcStackResourceALBTargetGroupLogicalID, + }, + }, nil), + ) + }, + wantedBackendSvc: &backendSvcDesc{ + Service: testSvc, + Type: "Backend Service", + App: testApp, + Configurations: []*ECSServiceConfig{ + { + ServiceConfig: &ServiceConfig{ + CPU: "256", + Environment: "test", + Memory: "512", + Platform: "LINUX/X86_64", + Port: "5000", + }, + Tasks: "1", + }, + }, + Routes: []*WebServiceRoute{ + { + Environment: "test", + URL: "http://us-west-1.elb.amazonaws.internal/mySvc", + }, + }, + ServiceDiscovery: []*ServiceDiscovery{ + { + Environment: []string{"test"}, + Namespace: "jobs.test.phonetool.local:5000", + }, + }, + Variables: []*containerEnvVar{ + { + envVar: &envVar{ + Environment: "test", + Name: "COPILOT_ENVIRONMENT_NAME", + Value: "test", + }, + Container: "container", + }, + }, + Secrets: []*secret{ + { + Name: "GITHUB_WEBHOOK_SECRET", + Container: "container", + Environment: "test", + ValueFrom: "GH_WEBHOOK_SECRET", + }, + }, + Resources: map[string][]*stack.Resource{ + "test": { + { + LogicalID: "TargetGroup", + }, + }, + }, + environments: []string{"test"}, + }, + }, } for name, tc := range testCases { @@ -423,7 +551,7 @@ Resources prod AWS::EC2::SecurityGroupIngress ContainerSecurityGroupIngressFromPublicALB `, - wantedJSONString: "{\"service\":\"my-svc\",\"type\":\"Backend Service\",\"application\":\"my-app\",\"configurations\":[{\"environment\":\"test\",\"port\":\"80\",\"cpu\":\"256\",\"memory\":\"512\",\"platform\":\"LINUX/X86_64\",\"tasks\":\"1\"},{\"environment\":\"prod\",\"port\":\"5000\",\"cpu\":\"512\",\"memory\":\"1024\",\"platform\":\"LINUX/ARM64\",\"tasks\":\"3\"}],\"serviceDiscovery\":[{\"environment\":[\"test\"],\"namespace\":\"http://my-svc.test.my-app.local:5000\"},{\"environment\":[\"prod\"],\"namespace\":\"http://my-svc.prod.my-app.local:5000\"}],\"variables\":[{\"environment\":\"prod\",\"name\":\"COPILOT_ENVIRONMENT_NAME\",\"value\":\"prod\",\"container\":\"container\"},{\"environment\":\"test\",\"name\":\"COPILOT_ENVIRONMENT_NAME\",\"value\":\"test\",\"container\":\"container\"}],\"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", + wantedJSONString: "{\"service\":\"my-svc\",\"type\":\"Backend Service\",\"application\":\"my-app\",\"configurations\":[{\"environment\":\"test\",\"port\":\"80\",\"cpu\":\"256\",\"memory\":\"512\",\"platform\":\"LINUX/X86_64\",\"tasks\":\"1\"},{\"environment\":\"prod\",\"port\":\"5000\",\"cpu\":\"512\",\"memory\":\"1024\",\"platform\":\"LINUX/ARM64\",\"tasks\":\"3\"}],\"routes\":null,\"serviceDiscovery\":[{\"environment\":[\"test\"],\"namespace\":\"http://my-svc.test.my-app.local:5000\"},{\"environment\":[\"prod\"],\"namespace\":\"http://my-svc.prod.my-app.local:5000\"}],\"variables\":[{\"environment\":\"prod\",\"name\":\"COPILOT_ENVIRONMENT_NAME\",\"value\":\"prod\",\"container\":\"container\"},{\"environment\":\"test\",\"name\":\"COPILOT_ENVIRONMENT_NAME\",\"value\":\"test\",\"container\":\"container\"}],\"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", }, } diff --git a/internal/pkg/describe/lb_web_service.go b/internal/pkg/describe/lb_web_service.go index 1609a038205..377873de8c0 100644 --- a/internal/pkg/describe/lb_web_service.go +++ b/internal/pkg/describe/lb_web_service.go @@ -28,9 +28,9 @@ import ( ) const ( - envOutputPublicLoadBalancerDNSName = "PublicLoadBalancerDNSName" - envOutputSubdomain = "EnvironmentSubdomain" - svcParamHTTPSEnabled = "HTTPSEnabled" + envOutputPublicLoadBalancerDNSName = "PublicLoadBalancerDNSName" + envOutputInternalLoadBalancerDNSName = "InternalLoadBalancerDNSName" + envOutputSubdomain = "EnvironmentSubdomain" svcStackResourceALBTargetGroupLogicalID = "TargetGroup" svcStackResourceNLBTargetGroupLogicalID = "NLBTargetGroup" @@ -134,13 +134,13 @@ func (d *LBWebServiceDescriber) Describe() (HumanJSONStringer, error) { if err != nil { return nil, err } - webServiceURI, err := d.URI(env) + uri, err := d.URI(env) if err != nil { return nil, fmt.Errorf("retrieve service URI: %w", err) } routes = append(routes, &WebServiceRoute{ Environment: env, - URL: webServiceURI, + URL: uri.URI, }) containerPlatform, err := svcDescr.Platform() if err != nil { diff --git a/internal/pkg/describe/uri.go b/internal/pkg/describe/uri.go index ef24091db99..36a40a12809 100644 --- a/internal/pkg/describe/uri.go +++ b/internal/pkg/describe/uri.go @@ -13,13 +13,27 @@ import ( "github.com/aws/copilot-cli/internal/pkg/manifest" ) +type URIAccessType int + +const ( + URIAccessTypeNone URIAccessType = iota + URIAccessTypeInternet + URIAccessTypeInternal + URIAccessTypeServiceDiscovery +) + var ( fmtSvcDiscoveryEndpointWithPort = "%s.%s:%s" // Format string of the form {svc}.{endpoint}:{port} ) -// ReachableService represents a service describe that has an endpoint. +type URI struct { + URI string + AccessType URIAccessType +} + +// ReachableService represents a service describer that has an endpoint. type ReachableService interface { - URI(env string) (string, error) + URI(env string) (URI, error) } // NewReachableService returns a ReachableService based on the type of the service. @@ -46,22 +60,19 @@ func NewReachableService(app, svc string, store ConfigStoreSvc) (ReachableServic } // URI returns the LBWebServiceURI to identify this service uniquely given an environment name. -func (d *LBWebServiceDescriber) URI(envName string) (string, error) { +func (d *LBWebServiceDescriber) URI(envName string) (URI, error) { svcDescr, err := d.initECSServiceDescribers(envName) if err != nil { - return "", err + return URI{}, err } envDescr, err := d.initEnvDescribers(envName) if err != nil { - return "", err + return URI{}, err } - var ( - albEnabled bool - nlbEnabled bool - ) + var albEnabled, nlbEnabled bool resources, err := svcDescr.ServiceStackResources() if err != nil { - return "", fmt.Errorf("get stack resources for service %s: %w", d.svc, err) + return URI{}, fmt.Errorf("get stack resources for service %s: %w", d.svc, err) } for _, resource := range resources { if resource.LogicalID == svcStackResourceALBTargetGroupLogicalID { @@ -74,9 +85,17 @@ func (d *LBWebServiceDescriber) URI(envName string) (string, error) { var uri LBWebServiceURI if albEnabled { - albURI, err := d.albURI(envName, svcDescr, envDescr) + albDescr := &albDescriber{ + svc: d.svc, + env: envName, + svcDescriber: svcDescr, + envDescriber: envDescr, + initLBDescriber: d.initLBDescriber, + envDNSNameKey: envOutputPublicLoadBalancerDNSName, + } + albURI, err := albDescr.uri() if err != nil { - return "", err + return URI{}, err } uri.albURI = albURI } @@ -84,54 +103,14 @@ func (d *LBWebServiceDescriber) URI(envName string) (string, error) { if nlbEnabled { nlbURI, err := d.nlbURI(envName, svcDescr, envDescr) if err != nil { - return "", err + return URI{}, err } uri.nlbURI = nlbURI } - return uri.String(), nil -} - -func (d *LBWebServiceDescriber) albURI(envName string, svcDescr ecsDescriber, envDescr envDescriber) (albURI, error) { - svcParams, err := svcDescr.Params() - if err != nil { - return albURI{}, fmt.Errorf("get stack parameters for service %s: %w", d.svc, err) - } - path := svcParams[stack.WorkloadRulePathParamKey] - isHTTPS, _ := svcParams[svcParamHTTPSEnabled] - if isHTTPS != "true" { - envOutputs, err := envDescr.Outputs() - if err != nil { - return albURI{}, fmt.Errorf("get stack outputs for environment %s: %w", envName, err) - } - return albURI{ - DNSNames: []string{envOutputs[envOutputPublicLoadBalancerDNSName]}, - Path: path, - }, nil - } - svcResources, err := svcDescr.ServiceStackResources() - if err != nil { - return albURI{}, fmt.Errorf("get stack resources for service %s: %w", d.svc, err) - } - var httpsRuleARN string - for _, resource := range svcResources { - if resource.LogicalID == svcStackResourceHTTPSListenerRuleLogicalID && - resource.Type == svcStackResourceHTTPSListenerRuleResourceType { - httpsRuleARN = resource.PhysicalID - } - } - lbDescr, err := d.initLBDescriber(envName) - if err != nil { - return albURI{}, nil - } - dnsNames, err := lbDescr.ListenerRuleHostHeaders(httpsRuleARN) - if err != nil { - return albURI{}, fmt.Errorf("get host headers for listener rule %s: %w", httpsRuleARN, err) - } - return albURI{ - HTTPS: true, - DNSNames: dnsNames, - Path: path, + return URI{ + URI: uri.String(), + AccessType: URIAccessTypeInternet, }, nil } @@ -172,48 +151,133 @@ func (d *LBWebServiceDescriber) nlbURI(envName string, svcDescr ecsDescriber, en // URI returns the service discovery namespace and is used to make // BackendServiceDescriber have the same signature as WebServiceDescriber. -func (d *BackendServiceDescriber) URI(envName string) (string, error) { +func (d *BackendServiceDescriber) URI(envName string) (URI, error) { svcDescr, err := d.initECSServiceDescribers(envName) if err != nil { - return "", err + return URI{}, err + } + envDescr, err := d.initEnvDescribers(envName) + if err != nil { + return URI{}, err + } + resources, err := svcDescr.ServiceStackResources() + if err != nil { + return URI{}, fmt.Errorf("get stack resources for service %s: %w", d.svc, err) + } + for _, res := range resources { + if res.LogicalID == svcStackResourceALBTargetGroupLogicalID { + albDescr := &albDescriber{ + svc: d.svc, + env: envName, + svcDescriber: svcDescr, + envDescriber: envDescr, + initLBDescriber: d.initLBDescriber, + envDNSNameKey: envOutputInternalLoadBalancerDNSName, + } + albURI, err := albDescr.uri() + if err != nil { + return URI{}, err + } + return URI{ + URI: english.OxfordWordSeries(albURI.strings(), "or"), + AccessType: URIAccessTypeInternal, + }, nil + } } + svcStackParams, err := svcDescr.Params() if err != nil { - return "", fmt.Errorf("get stack parameters for environment %s: %w", envName, err) + return URI{}, fmt.Errorf("get stack parameters for environment %s: %w", envName, err) } port := svcStackParams[stack.WorkloadContainerPortParamKey] if port == stack.NoExposedContainerPort { - return BlankServiceDiscoveryURI, nil - } - envDescr, err := d.initEnvDescribers(envName) - if err != nil { - return "", err + return URI{ + URI: BlankServiceDiscoveryURI, + AccessType: URIAccessTypeNone, + }, nil } endpoint, err := envDescr.ServiceDiscoveryEndpoint() if err != nil { - return "nil", fmt.Errorf("retrieve service discovery endpoint for environment %s: %w", envName, err) + return URI{}, fmt.Errorf("retrieve service discovery endpoint for environment %s: %w", envName, err) } s := serviceDiscovery{ Service: d.svc, Port: port, Endpoint: endpoint, } - return s.String(), nil + return URI{ + URI: s.String(), + AccessType: URIAccessTypeServiceDiscovery, + }, nil +} + +type albDescriber struct { + svc string + env string + svcDescriber ecsDescriber + envDescriber envDescriber + initLBDescriber func(string) (lbDescriber, error) + envDNSNameKey string +} + +func (d *albDescriber) uri() (albURI, error) { + svcParams, err := d.svcDescriber.Params() + if err != nil { + return albURI{}, fmt.Errorf("get stack parameters for service %s: %w", d.svc, err) + } + path := svcParams[stack.WorkloadRulePathParamKey] + if svcParams[stack.WorkloadHTTPSParamKey] != "true" { + envOutputs, err := d.envDescriber.Outputs() + if err != nil { + return albURI{}, fmt.Errorf("get stack outputs for environment %s: %w", d.env, err) + } + return albURI{ + DNSNames: []string{envOutputs[d.envDNSNameKey]}, + Path: path, + }, nil + } + svcResources, err := d.svcDescriber.ServiceStackResources() + if err != nil { + return albURI{}, fmt.Errorf("get stack resources for service %s: %w", d.svc, err) + } + var httpsRuleARN string + for _, resource := range svcResources { + if resource.LogicalID == svcStackResourceHTTPSListenerRuleLogicalID && + resource.Type == svcStackResourceHTTPSListenerRuleResourceType { + httpsRuleARN = resource.PhysicalID + } + } + lbDescr, err := d.initLBDescriber(d.env) + if err != nil { + return albURI{}, nil + } + dnsNames, err := lbDescr.ListenerRuleHostHeaders(httpsRuleARN) + if err != nil { + return albURI{}, fmt.Errorf("get host headers for listener rule %s: %w", httpsRuleARN, err) + } + return albURI{ + HTTPS: true, + DNSNames: dnsNames, + Path: path, + }, nil } // URI returns the WebServiceURI to identify this service uniquely given an environment name. -func (d *RDWebServiceDescriber) URI(envName string) (string, error) { +func (d *RDWebServiceDescriber) URI(envName string) (URI, error) { describer, err := d.initAppRunnerDescriber(envName) if err != nil { - return "", err + return URI{}, err } serviceURL, err := describer.ServiceURL() if err != nil { - return "", fmt.Errorf("get outputs for service %s: %w", d.svc, err) + return URI{}, fmt.Errorf("get outputs for service %s: %w", d.svc, err) } - return serviceURL, nil + return URI{ + URI: serviceURL, + AccessType: URIAccessTypeInternet, + }, nil } // LBWebServiceURI represents the unique identifier to access a load balanced web service. @@ -234,23 +298,27 @@ type nlbURI struct { } func (u *LBWebServiceURI) String() string { + uris := u.albURI.strings() + for _, dnsName := range u.nlbURI.DNSNames { + uris = append(uris, fmt.Sprintf("%s:%s", dnsName, u.nlbURI.Port)) + } + return english.OxfordWordSeries(uris, "or") +} + +func (u *albURI) strings() []string { var uris []string - for _, dnsName := range u.albURI.DNSNames { + for _, dnsName := range u.DNSNames { protocol := "http://" - if u.albURI.HTTPS { + if u.HTTPS { protocol = "https://" } path := "" - if u.albURI.Path != "/" { - path = fmt.Sprintf("/%s", u.albURI.Path) + if u.Path != "/" { + path = fmt.Sprintf("/%s", u.Path) } - uris = append(uris, fmt.Sprintf("%s%s%s", protocol, dnsName, path)) - } - - for _, dnsName := range u.nlbURI.DNSNames { - uris = append(uris, fmt.Sprintf("%s:%s", dnsName, u.nlbURI.Port)) + uris = append(uris, protocol+dnsName+path) } - return english.OxfordWordSeries(uris, "or") + return uris } type serviceDiscovery struct { diff --git a/internal/pkg/describe/uri_test.go b/internal/pkg/describe/uri_test.go index 6ddc7a677f5..d8fc02a36a6 100644 --- a/internal/pkg/describe/uri_test.go +++ b/internal/pkg/describe/uri_test.go @@ -120,7 +120,6 @@ func TestLBWebServiceDescriber_URI(t *testing.T) { Return([]string{"jobs.test.phonetool.com", "phonetool.com"}, nil), ) }, - wantedURI: "https://jobs.test.phonetool.com or https://phonetool.com", }, "http web service": { @@ -306,57 +305,125 @@ func TestLBWebServiceDescriber_URI(t *testing.T) { require.EqualError(t, err, tc.wantedError.Error()) } else { require.NoError(t, err) - require.Equal(t, tc.wantedURI, actual) + require.Equal(t, tc.wantedURI, actual.URI) } }) } } func TestBackendServiceDescriber_URI(t *testing.T) { - t.Run("should return a blank service discovery URI if there is no port exposed", func(t *testing.T) { - // GIVEN - ctrl := gomock.NewController(t) - defer ctrl.Finish() - m := mocks.NewMockecsDescriber(ctrl) - m.EXPECT().Params().Return(map[string]string{ - stack.WorkloadContainerPortParamKey: stack.NoExposedContainerPort, // No port is set for the backend service. - }, nil) - - d := &BackendServiceDescriber{ - initECSServiceDescribers: func(s string) (ecsDescriber, error) { return m, nil }, - } - - // WHEN - actual, err := d.URI("test") - - // THEN - require.NoError(t, err) - require.Equal(t, BlankServiceDiscoveryURI, actual) - }) - t.Run("should return service discovery endpoint if port is exposed", func(t *testing.T) { - // GIVEN - ctrl := gomock.NewController(t) - defer ctrl.Finish() - mockSvcStack := mocks.NewMockecsDescriber(ctrl) - mockSvcStack.EXPECT().Params().Return(map[string]string{ - stack.WorkloadContainerPortParamKey: "8080", - }, nil) - mockEnvStack := mocks.NewMockenvDescriber(ctrl) - mockEnvStack.EXPECT().ServiceDiscoveryEndpoint().Return("test.app.local", nil) - - d := &BackendServiceDescriber{ - svc: "hello", - initECSServiceDescribers: func(s string) (ecsDescriber, error) { return mockSvcStack, nil }, - initEnvDescribers: func(s string) (envDescriber, error) { return mockEnvStack, nil }, - } - - // WHEN - actual, err := d.URI("test") - - // THEN - require.NoError(t, err) - require.Equal(t, "hello.test.app.local:8080", actual) - }) + const ( + testApp = "phonetool" + testEnv = "test" + testSvc = "my-svc" + testEnvInternalDNSName = "abc.us-west-1.elb.amazonaws.internal" + ) + testCases := map[string]struct { + setupMocks func(mocks lbWebSvcDescriberMocks) + + wantedURI string + wantedError error + }{ + "should return a blank service discovery URI if there is no port exposed": { + setupMocks: func(m lbWebSvcDescriberMocks) { + m.ecsDescriber.EXPECT().ServiceStackResources().Return(nil, nil) + m.ecsDescriber.EXPECT().Params().Return(map[string]string{ + stack.WorkloadContainerPortParamKey: stack.NoExposedContainerPort, // No port is set for the backend service. + }, nil) + }, + wantedURI: BlankServiceDiscoveryURI, + }, + "should return service discovery endpoint if port is exposed": { + setupMocks: func(m lbWebSvcDescriberMocks) { + m.ecsDescriber.EXPECT().ServiceStackResources().Return(nil, nil) + m.ecsDescriber.EXPECT().Params().Return(map[string]string{ + stack.WorkloadContainerPortParamKey: "8080", + }, nil) + m.envDescriber.EXPECT().ServiceDiscoveryEndpoint().Return("test.app.local", nil) + }, + wantedURI: "my-svc.test.app.local:8080", + }, + "internal url http": { + setupMocks: func(m lbWebSvcDescriberMocks) { + gomock.InOrder( + m.ecsDescriber.EXPECT().ServiceStackResources().Return([]*describeStack.Resource{ + { + LogicalID: svcStackResourceALBTargetGroupLogicalID, + }, + }, nil), + m.ecsDescriber.EXPECT().Params().Return(map[string]string{ + stack.WorkloadRulePathParamKey: "mySvc", + }, nil), + m.envDescriber.EXPECT().Outputs().Return(map[string]string{ + envOutputInternalLoadBalancerDNSName: testEnvInternalDNSName, + }, nil), + ) + }, + wantedURI: "http://abc.us-west-1.elb.amazonaws.internal/mySvc", + }, + "internal url https": { + setupMocks: func(m lbWebSvcDescriberMocks) { + gomock.InOrder( + m.ecsDescriber.EXPECT().ServiceStackResources().Return([]*describeStack.Resource{ + { + LogicalID: svcStackResourceALBTargetGroupLogicalID, + }, + }, nil), + m.ecsDescriber.EXPECT().Params().Return(map[string]string{ + stack.WorkloadRulePathParamKey: "/", + stack.WorkloadHTTPSParamKey: "true", + }, nil), + m.ecsDescriber.EXPECT().ServiceStackResources().Return([]*describeStack.Resource{ + { + LogicalID: svcStackResourceHTTPSListenerRuleLogicalID, + Type: svcStackResourceHTTPSListenerRuleResourceType, + PhysicalID: "mockRuleARN", + }, + }, nil), + m.lbDescriber.EXPECT().ListenerRuleHostHeaders("mockRuleARN"). + Return([]string{"jobs.test.phonetool.com", "phonetool.com"}, nil), + ) + }, + wantedURI: "https://jobs.test.phonetool.com or https://phonetool.com", + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + // GIVEN + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvcDescriber := mocks.NewMockecsDescriber(ctrl) + mockEnvDescriber := mocks.NewMockenvDescriber(ctrl) + mockLBDescriber := mocks.NewMocklbDescriber(ctrl) + mocks := lbWebSvcDescriberMocks{ + ecsDescriber: mockSvcDescriber, + envDescriber: mockEnvDescriber, + lbDescriber: mockLBDescriber, + } + + tc.setupMocks(mocks) + + d := &BackendServiceDescriber{ + app: testApp, + svc: testSvc, + initECSServiceDescribers: func(s string) (ecsDescriber, error) { return mockSvcDescriber, nil }, + initEnvDescribers: func(s string) (envDescriber, error) { return mockEnvDescriber, nil }, + initLBDescriber: func(s string) (lbDescriber, error) { return mockLBDescriber, nil }, + } + + // WHEN + actual, err := d.URI(testEnv) + + // THEN + if tc.wantedError != nil { + require.EqualError(t, err, tc.wantedError.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tc.wantedURI, actual.URI) + } + }) + } } func TestRDWebServiceDescriber_URI(t *testing.T) { @@ -419,7 +486,7 @@ func TestRDWebServiceDescriber_URI(t *testing.T) { require.EqualError(t, err, tc.wantedError.Error()) } else { require.NoError(t, err) - require.Equal(t, tc.wantedURI, actual) + require.Equal(t, tc.wantedURI, actual.URI) } }) }