internal/pkg/deploy/cloudformation/pipeline.go (109 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package cloudformation provides functionality to deploy ECS resources with AWS CloudFormation.
// This file defines API for deploying a pipeline.
package cloudformation
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/copilot-cli/internal/pkg/aws/cloudformation"
"github.com/aws/copilot-cli/internal/pkg/deploy"
"github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation/stack"
)
const (
sourceStage = "Source"
connectionARNKey = "PipelineConnectionARN"
fmtPipelineCfnTemplateName = "%s.pipeline.stack.yml"
cfnLogicalResourceIDPipeline = "Pipeline"
cfnResourceTypePipeline = "AWS::CodePipeline::Pipeline"
)
// PipelineExists checks if the pipeline with the provided config exists.
func (cf CloudFormation) PipelineExists(stackConfig StackConfiguration) (bool, error) {
_, err := cf.cfnClient.Describe(stackConfig.StackName())
if err != nil {
var stackNotFound *cloudformation.ErrStackNotFound
if !errors.As(err, &stackNotFound) {
return false, err
}
return false, nil
}
return true, nil
}
// CreatePipeline sets up a new CodePipeline for deploying services.
func (cf CloudFormation) CreatePipeline(bucketName string, stackConfig StackConfiguration) error {
templateURL, err := cf.pushTemplateToS3Bucket(bucketName, stackConfig)
if err != nil {
return err
}
s, err := toStackFromS3(stackConfig, templateURL)
if err != nil {
return err
}
err = cf.cfnClient.CreateAndWait(s)
if err != nil {
return err
}
output, err := cf.cfnClient.Outputs(s)
if err != nil {
return err
}
// If the pipeline has a PipelineConnectionARN in the output map, indicating that it is has a CodeStarConnections source provider, the user needs to update the connection status; Copilot will wait until that happens.
if output[connectionARNKey] == "" {
return nil
}
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(45*time.Minute))
defer cancel()
if err = cf.codeStarClient.WaitUntilConnectionStatusAvailable(ctx, output[connectionARNKey]); err != nil {
return err
}
pipelineResourceName, err := cf.pipelinePhysicalResourceID(stackConfig.StackName())
if err != nil {
return err
}
if err = cf.cpClient.RetryStageExecution(pipelineResourceName, sourceStage); err != nil {
return err
}
return nil
}
func (cf CloudFormation) pipelinePhysicalResourceID(stackName string) (string, error) {
stackResources, err := cf.cfnClient.StackResources(stackName)
if err != nil {
return "", err
}
for _, resource := range stackResources {
if aws.StringValue(resource.LogicalResourceId) == cfnLogicalResourceIDPipeline && aws.StringValue(resource.ResourceType) == cfnResourceTypePipeline {
return aws.StringValue(resource.PhysicalResourceId), nil
}
}
return "", fmt.Errorf(`cannot find a resource in stack %s with logical ID "%s" of type "%s"`, stackName, cfnLogicalResourceIDPipeline, cfnResourceTypePipeline)
}
// UpdatePipeline updates an existing CodePipeline for deploying services.
func (cf CloudFormation) UpdatePipeline(bucketName string, stackConfig StackConfiguration) error {
templateURL, err := cf.pushTemplateToS3Bucket(bucketName, stackConfig)
if err != nil {
return err
}
s, err := toStackFromS3(stackConfig, templateURL)
if err != nil {
return err
}
if err := cf.cfnClient.UpdateAndWait(s); err != nil {
var errNoUpdates *cloudformation.ErrChangeSetEmpty
if errors.As(err, &errNoUpdates) {
return nil
}
return fmt.Errorf("update pipeline: %w", err)
}
return nil
}
// DeletePipeline removes the CodePipeline stack.
func (cf CloudFormation) DeletePipeline(pipeline deploy.Pipeline) error {
return cf.cfnClient.DeleteAndWait(stack.NameForPipeline(pipeline.AppName, pipeline.Name, pipeline.IsLegacy))
}
func (cf CloudFormation) pushTemplateToS3Bucket(bucket string, config StackConfiguration) (string, error) {
template, err := config.Template()
if err != nil {
return "", fmt.Errorf("generate template: %w", err)
}
reader := strings.NewReader(template)
url, err := cf.s3Client.Upload(bucket, fmt.Sprintf(fmtPipelineCfnTemplateName, config.StackName()), reader)
if err != nil {
return "", fmt.Errorf("upload pipeline template to S3 bucket %s: %w", bucket, err)
}
return url, nil
}