internal/pkg/cli/deploy/backend.go (186 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
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"
awsecs "github.com/aws/copilot-cli/internal/pkg/aws/ecs"
"github.com/aws/copilot-cli/internal/pkg/aws/partitions"
"github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation"
"github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation/stack"
"github.com/aws/copilot-cli/internal/pkg/deploy/upload/customresource"
"github.com/aws/copilot-cli/internal/pkg/ecs"
"github.com/aws/copilot-cli/internal/pkg/manifest"
"github.com/aws/copilot-cli/internal/pkg/manifest/manifestinfo"
"github.com/aws/copilot-cli/internal/pkg/template"
)
type backendSvcDeployer struct {
*svcDeployer
elbGetter elbGetter
backendMft *manifest.BackendService
// Overriden in tests.
aliasCertValidator aliasCertValidator
newStack func() cloudformation.StackConfiguration
}
// NewBackendDeployer is the constructor for backendSvcDeployer.
func NewBackendDeployer(in *WorkloadDeployerInput) (*backendSvcDeployer, error) {
in.customResources = backendCustomResources
svcDeployer, err := newSvcDeployer(in)
if err != nil {
return nil, err
}
bsMft, ok := in.Mft.(*manifest.BackendService)
if !ok {
return nil, fmt.Errorf("manifest is not of type %s", manifestinfo.BackendServiceType)
}
return &backendSvcDeployer{
svcDeployer: svcDeployer,
elbGetter: elbv2.New(svcDeployer.envSess),
backendMft: bsMft,
aliasCertValidator: acm.New(svcDeployer.envSess),
}, nil
}
func backendCustomResources(fs template.Reader) ([]*customresource.CustomResource, error) {
crs, err := customresource.Backend(fs)
if err != nil {
return nil, fmt.Errorf("read custom resources for a %q: %w", manifestinfo.BackendServiceType, err)
}
return crs, nil
}
// IsServiceAvailableInRegion checks if service type exist in the given region.
func (backendSvcDeployer) IsServiceAvailableInRegion(region string) (bool, error) {
return partitions.IsAvailableInRegion(awsecs.EndpointsID, region)
}
// UploadArtifacts uploads the deployment artifacts such as the container image, custom resources, addons and env files.
func (d *backendSvcDeployer) UploadArtifacts() (*UploadArtifactsOutput, error) {
return d.uploadArtifacts(d.buildAndPushContainerImages, d.uploadArtifactsToS3, d.uploadCustomResources)
}
// GenerateCloudFormationTemplate generates a CloudFormation template and parameters for a workload.
func (d *backendSvcDeployer) GenerateCloudFormationTemplate(in *GenerateCloudFormationTemplateInput) (
*GenerateCloudFormationTemplateOutput, error) {
output, err := d.stackConfiguration(&in.StackRuntimeConfiguration)
if err != nil {
return nil, err
}
return d.generateCloudFormationTemplate(output.conf)
}
// DeployWorkload deploys a backend service using CloudFormation.
func (d *backendSvcDeployer) DeployWorkload(in *DeployWorkloadInput) (ActionRecommender, error) {
stackConfigOutput, err := d.stackConfiguration(&in.StackRuntimeConfiguration)
if err != nil {
return nil, err
}
if err := d.deploy(in.Options, *stackConfigOutput); err != nil {
return nil, err
}
return noopActionRecommender{}, nil
}
func (d *backendSvcDeployer) stackConfiguration(in *StackRuntimeConfiguration) (*svcStackConfigurationOutput, error) {
rc, err := d.runtimeConfig(in)
if err != nil {
return nil, err
}
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 {
case d.newStack != nil:
conf = d.newStack()
default:
conf, err = stack.NewBackendService(stack.BackendServiceConfig{
App: d.app,
EnvManifest: d.envConfig,
Manifest: d.backendMft,
RawManifest: d.rawMft,
ArtifactBucketName: d.resources.S3Bucket,
ArtifactKey: d.resources.KMSKeyARN,
RuntimeConfig: *rc,
Addons: d.addons,
}, opts...)
if err != nil {
return nil, fmt.Errorf("create stack configuration: %w", err)
}
}
return &svcStackConfigurationOutput{
conf: cloudformation.WrapWithTemplateOverrider(conf, d.overrider),
svcUpdater: d.newSvcUpdater(func(s *session.Session) serviceForceUpdater {
return ecs.New(s)
}),
}, nil
}
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)
}
for idx, rule := range d.backendMft.HTTP.AdditionalRoutingRules {
if err := d.validateRuntimeRoutingRule(rule); err != nil {
return fmt.Errorf(`validate ALB runtime configuration for "http.additional_rules[%d]": %w`, idx, err)
}
}
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
}
hasImportedCerts := len(d.envConfig.HTTPConfig.Private.Certificates) != 0
switch {
case rule.Alias.IsEmpty() && hasImportedCerts:
return &errSvcWithNoALBAliasDeployingToEnvWithImportedCerts{
name: d.name,
envName: d.env.Name,
}
case rule.Alias.IsEmpty():
return nil
case !hasImportedCerts:
return fmt.Errorf(`cannot specify "alias" in an environment without imported certs`)
}
aliases, err := rule.Alias.ToStringSlice()
if err != nil {
return fmt.Errorf("convert aliases to string slice: %w", err)
}
if err := d.aliasCertValidator.ValidateCertAliases(aliases, d.envConfig.HTTPConfig.Private.Certificates); err != nil {
return fmt.Errorf("validate aliases against the imported certificate for env %s: %w", d.env.Name, err)
}
return nil
}