Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support importing internal ALBs for backend services #5490

Merged
merged 7 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion internal/pkg/cli/deploy/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package deploy

import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/copilot-cli/internal/pkg/aws/elbv2"

"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/copilot-cli/internal/pkg/aws/acm"
Expand All @@ -21,6 +23,7 @@ import (

type backendSvcDeployer struct {
*svcDeployer
elbGetter elbGetter
backendMft *manifest.BackendService

// Overriden in tests.
Expand All @@ -41,6 +44,7 @@ func NewBackendDeployer(in *WorkloadDeployerInput) (*backendSvcDeployer, error)
}
return &backendSvcDeployer{
svcDeployer: svcDeployer,
elbGetter: elbv2.New(svcDeployer.envSess),
backendMft: bsMft,
aliasCertValidator: acm.New(svcDeployer.envSess),
}, nil
Expand Down Expand Up @@ -94,6 +98,14 @@ func (d *backendSvcDeployer) stackConfiguration(in *StackRuntimeConfiguration) (
if err := d.validateALBRuntime(); err != nil {
return nil, err
}
var opts []stack.BackendServiceOption
if d.backendMft.HTTP.ImportedALB != nil {
lb, err := d.elbGetter.LoadBalancer(aws.StringValue(d.backendMft.HTTP.ImportedALB))
if err != nil {
return nil, err
}
opts = append(opts, stack.WithImportedInternalALB(lb))
}

var conf cloudformation.StackConfiguration
switch {
Expand All @@ -109,7 +121,7 @@ func (d *backendSvcDeployer) stackConfiguration(in *StackRuntimeConfiguration) (
ArtifactKey: d.resources.KMSKeyARN,
RuntimeConfig: *rc,
Addons: d.addons,
})
}, opts...)
if err != nil {
return nil, fmt.Errorf("create stack configuration: %w", err)
}
Expand All @@ -127,6 +139,9 @@ func (d *backendSvcDeployer) validateALBRuntime() error {
if d.backendMft.HTTP.IsEmpty() {
return nil
}
if err := d.validateImportedALBConfig(); err != nil {
return fmt.Errorf(`validate imported ALB configuration for "http": %w`, err)
}
if err := d.validateRuntimeRoutingRule(d.backendMft.HTTP.Main); err != nil {
return fmt.Errorf(`validate ALB runtime configuration for "http": %w`, err)
}
Expand All @@ -138,6 +153,37 @@ func (d *backendSvcDeployer) validateALBRuntime() error {
return nil
}

func (d *backendSvcDeployer) validateImportedALBConfig() error {
if d.backendMft.HTTP.ImportedALB == nil {
return nil
}
alb, err := d.elbGetter.LoadBalancer(aws.StringValue(d.backendMft.HTTP.ImportedALB))
if err != nil {
return fmt.Errorf(`retrieve load balancer %q: %w`, aws.StringValue(d.backendMft.HTTP.ImportedALB), err)
}
if alb.Scheme != "internal" {
return fmt.Errorf(`imported ALB %q for Backend Service %q should have "internal" Scheme value`, alb.ARN, aws.StringValue(d.backendMft.Name))
}
if len(alb.Listeners) == 0 {
return fmt.Errorf(`imported ALB %q must have at least one listener. For two listeners, one must be of protocol HTTP and the other of protocol HTTPS`, alb.ARN)
}
if len(alb.Listeners) == 1 {
return nil
}
var quantHTTP, quantHTTPS int
for _, listener := range alb.Listeners {
if listener.Protocol == "HTTP" {
quantHTTP += 1
} else if listener.Protocol == "HTTPS" {
quantHTTPS += 1
}
}
if quantHTTP != 1 || quantHTTPS != 1 {
return fmt.Errorf("imported ALB %q must have exactly one listener of protocol HTTP and exactly one listener of protocol HTTPS", alb.ARN)
}
return nil
}

func (d *backendSvcDeployer) validateRuntimeRoutingRule(rule manifest.RoutingRule) error {
if rule.IsEmpty() {
return nil
Expand Down
210 changes: 210 additions & 0 deletions internal/pkg/cli/deploy/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package deploy

import (
"errors"
"github.com/aws/copilot-cli/internal/pkg/aws/elbv2"
"testing"
"time"

Expand Down Expand Up @@ -254,6 +255,213 @@ func TestBackendSvcDeployer_stackConfiguration(t *testing.T) {
},
expectedErr: `validate ALB runtime configuration for "http.additional_rules[0]": cannot deploy service mock-svc without "alias" to environment mock-env with certificate imported`,
},
"failure if can't retrieve imported ALB": {
App: &config.Application{
Name: mockAppName,
},
Env: &config.Environment{
Name: mockEnvName,
},
Manifest: &manifest.BackendService{
BackendServiceConfig: manifest.BackendServiceConfig{
HTTP: manifest.HTTP{
Main: manifest.RoutingRule{
Path: aws.String("/"),
},
ImportedALB: aws.String("mockALB"),
},
},
},
setupMocks: func(m *deployMocks) {
m.mockEndpointGetter.EXPECT().ServiceDiscoveryEndpoint().Return(mockAppName+".local", nil)
m.mockEnvVersionGetter.EXPECT().Version().Return("v1.42.0", nil)
m.mockELBGetter.EXPECT().LoadBalancer("mockALB").Return(nil, errors.New("some error"))
},
expectedErr: `validate imported ALB configuration for "http": retrieve load balancer "mockALB": some error`,
},
"failure if imported ALB has 'internet-facing' not 'internal' scheme": {
App: &config.Application{
Name: mockAppName,
},
Env: &config.Environment{
Name: mockEnvName,
},
Manifest: &manifest.BackendService{
Workload: manifest.Workload{
Name: aws.String("be"),
},
BackendServiceConfig: manifest.BackendServiceConfig{
HTTP: manifest.HTTP{
Main: manifest.RoutingRule{
Path: aws.String("/"),
},
ImportedALB: aws.String("mockALB"),
},
},
},
setupMocks: func(m *deployMocks) {
m.mockEndpointGetter.EXPECT().ServiceDiscoveryEndpoint().Return(mockAppName+".local", nil)
m.mockEnvVersionGetter.EXPECT().Version().Return("v1.42.0", nil)
m.mockELBGetter.EXPECT().LoadBalancer("mockALB").Return(&elbv2.LoadBalancer{
ARN: "mockALBARN",
Name: "mockALB",
Scheme: "internet-facing",
}, nil)
},
expectedErr: `validate imported ALB configuration for "http": imported ALB "mockALBARN" for Backend Service "be" should have "internal" Scheme value`,
},
"failure if imported ALB has no listeners": {
App: &config.Application{
Name: mockAppName,
},
Env: &config.Environment{
Name: mockEnvName,
},
Manifest: &manifest.BackendService{
BackendServiceConfig: manifest.BackendServiceConfig{
HTTP: manifest.HTTP{
Main: manifest.RoutingRule{
Path: aws.String("/"),
},
ImportedALB: aws.String("mockALB"),
},
},
},
setupMocks: func(m *deployMocks) {
m.mockEndpointGetter.EXPECT().ServiceDiscoveryEndpoint().Return(mockAppName+".local", nil)
m.mockEnvVersionGetter.EXPECT().Version().Return("v1.42.0", nil)
m.mockELBGetter.EXPECT().LoadBalancer("mockALB").Return(&elbv2.LoadBalancer{
ARN: "mockALBARN",
Name: "mockALB",
Scheme: "internal",
Listeners: []elbv2.Listener{},
}, nil)
},
expectedErr: `validate imported ALB configuration for "http": imported ALB "mockALBARN" must have at least one listener. For two listeners, one must be of protocol HTTP and the other of protocol HTTPS`,
},
"failure if imported ALB has more than 2 listeners": {
App: &config.Application{
Name: mockAppName,
},
Env: &config.Environment{
Name: mockEnvName,
},
Manifest: &manifest.BackendService{
BackendServiceConfig: manifest.BackendServiceConfig{
HTTP: manifest.HTTP{
Main: manifest.RoutingRule{
Path: aws.String("/"),
},
ImportedALB: aws.String("mockALB"),
},
},
},
setupMocks: func(m *deployMocks) {
m.mockEndpointGetter.EXPECT().ServiceDiscoveryEndpoint().Return(mockAppName+".local", nil)
m.mockEnvVersionGetter.EXPECT().Version().Return("v1.42.0", nil)
m.mockELBGetter.EXPECT().LoadBalancer("mockALB").Return(&elbv2.LoadBalancer{
ARN: "mockALBARN",
Name: "mockALB",
Scheme: "internal",
Listeners: []elbv2.Listener{
{
ARN: "default",
Port: 0,
Protocol: "something",
},
{
ARN: "second",
Port: 80,
Protocol: "http",
},
{
ARN: "third",
Port: 443,
Protocol: "https",
}},
}, nil)
},
expectedErr: `validate imported ALB configuration for "http": imported ALB "mockALBARN" must have exactly one listener of protocol HTTP and exactly one listener of protocol HTTPS`,
},
"failure if imported ALB has two listeners but they don't have HTTP and HTTPS protocols": {
App: &config.Application{
Name: mockAppName,
},
Env: &config.Environment{
Name: mockEnvName,
},
Manifest: &manifest.BackendService{
Workload: manifest.Workload{
Name: aws.String("be"),
},
BackendServiceConfig: manifest.BackendServiceConfig{
HTTP: manifest.HTTP{
Main: manifest.RoutingRule{
Path: aws.String("/"),
},
ImportedALB: aws.String("mockALB"),
},
},
},
setupMocks: func(m *deployMocks) {
m.mockEndpointGetter.EXPECT().ServiceDiscoveryEndpoint().Return(mockAppName+".local", nil)
m.mockEnvVersionGetter.EXPECT().Version().Return("v1.42.0", nil)
m.mockELBGetter.EXPECT().LoadBalancer("mockALB").Return(&elbv2.LoadBalancer{
ARN: "mockALBARN",
Name: "mockALB",
Scheme: "internal",
Listeners: []elbv2.Listener{
{
ARN: "default",
Port: 0,
Protocol: "something",
},
{
ARN: "second",
Port: 80,
Protocol: "boop",
}},
}, nil)
},
expectedErr: `validate imported ALB configuration for "http": imported ALB "mockALBARN" must have exactly one listener of protocol HTTP and exactly one listener of protocol HTTPS`,
},
"success imported ALB": {
App: &config.Application{
Name: mockAppName,
},
Env: &config.Environment{
Name: mockEnvName,
},
Manifest: &manifest.BackendService{
Workload: manifest.Workload{
Name: aws.String("be"),
},
BackendServiceConfig: manifest.BackendServiceConfig{
HTTP: manifest.HTTP{
Main: manifest.RoutingRule{
Path: aws.String("/"),
},
ImportedALB: aws.String("mockALB"),
},
},
},
setupMocks: func(m *deployMocks) {
m.mockEndpointGetter.EXPECT().ServiceDiscoveryEndpoint().Return(mockAppName+".local", nil)
m.mockEnvVersionGetter.EXPECT().Version().Return("v1.42.0", nil)
m.mockELBGetter.EXPECT().LoadBalancer("mockALB").Return(&elbv2.LoadBalancer{
ARN: "mockALBARN",
Name: "mockALB",
Scheme: "internal",
Listeners: []elbv2.Listener{
{
ARN: "yarn",
Port: 80,
Protocol: "HTTP",
},
},
}, nil).Times(2)
},
},
"success if env has imported certs but alb not configured": {
App: &config.Application{
Name: mockAppName,
Expand Down Expand Up @@ -281,6 +489,7 @@ func TestBackendSvcDeployer_stackConfiguration(t *testing.T) {
mockEndpointGetter: mocks.NewMockendpointGetter(ctrl),
mockValidator: mocks.NewMockaliasCertValidator(ctrl),
mockEnvVersionGetter: mocks.NewMockversionGetter(ctrl),
mockELBGetter: mocks.NewMockelbGetter(ctrl),
}
if tc.setupMocks != nil {
tc.setupMocks(m)
Expand All @@ -305,6 +514,7 @@ func TestBackendSvcDeployer_stackConfiguration(t *testing.T) {
return nil
},
},
elbGetter: m.mockELBGetter,
backendMft: tc.Manifest,
aliasCertValidator: m.mockValidator,
newStack: func() cloudformation.StackConfiguration {
Expand Down
1 change: 1 addition & 0 deletions internal/pkg/cli/deploy/workload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type deployMocks struct {
mockValidator *mocks.MockaliasCertValidator
mockLabeledTermPrinter *mocks.MockLabeledTermPrinter
mockdockerEngineRunChecker *mocks.MockdockerEngineRunChecker
mockELBGetter *mocks.MockelbGetter
}

type mockTemplateFS struct {
Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/cli/run_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -1102,7 +1102,7 @@ func (h *hostDiscoverer) Hosts(ctx context.Context) ([]orchestrator.Host, error)

var hosts []orchestrator.Host
for _, svc := range svcs {
// find the primary deployment with service connect enabled
// find the primary deployment with Service Connect enabled
idx := slices.IndexFunc(svc.Deployments, func(dep *sdkecs.Deployment) bool {
return aws.StringValue(dep.Status) == "PRIMARY" && aws.BoolValue(dep.ServiceConnectConfiguration.Enabled)
})
Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/cli/svc_deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ func (o *deploySvcOpts) uriRecommendedActions() ([]string, error) {
case describe.URIAccessTypeServiceDiscovery:
network = "with service discovery."
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we fix this too 🥺

Copy link
Contributor Author

Choose a reason for hiding this comment

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

case describe.URIAccessTypeServiceConnect:
network = "with service connect."
network = "with Service Connect."
case describe.URIAccessTypeNone:
return []string{}, nil
}
Expand Down
Loading
Loading