From 6629d8e4cf4ca92423a044e85cf77ea873ecb0f8 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 16 Jul 2020 16:07:26 +0200 Subject: [PATCH] `up` can update an existing stack using CloudFormation Changeset Signed-off-by: Nicolas De Loof --- pkg/amazon/backend/up.go | 29 +++++++++++++------- pkg/amazon/sdk/api.go | 2 ++ pkg/amazon/sdk/sdk.go | 59 ++++++++++++++++++++++++++++++++++++++-- pkg/compose/types.go | 1 + 4 files changed, 78 insertions(+), 13 deletions(-) diff --git a/pkg/amazon/backend/up.go b/pkg/amazon/backend/up.go index 8b975dc..3ea21d1 100644 --- a/pkg/amazon/backend/up.go +++ b/pkg/amazon/backend/up.go @@ -26,14 +26,6 @@ func (b *Backend) Up(ctx context.Context, options cli.ProjectOptions) error { return err } - update, err := b.api.StackExists(ctx, project.Name) - if err != nil { - return err - } - if update { - return fmt.Errorf("we do not (yet) support updating an existing CloudFormation stack") - } - template, err := b.Convert(project) if err != nil { return err @@ -62,17 +54,34 @@ func (b *Backend) Up(ctx context.Context, options cli.ProjectOptions) error { ParameterLoadBalancerARN: lb, } - err = b.api.CreateStack(ctx, project.Name, template, parameters) + update, err := b.api.StackExists(ctx, project.Name) if err != nil { return err } + operation := compose.StackCreate + if update { + operation = compose.StackUpdate + changeset, err := b.api.CreateChangeSet(ctx, project.Name, template, parameters) + if err != nil { + return err + } + err = b.api.UpdateStack(ctx, changeset) + if err != nil { + return err + } + } else { + err = b.api.CreateStack(ctx, project.Name, template, parameters) + if err != nil { + return err + } + } fmt.Println() w := console.NewProgressWriter() for k := range template.Resources { w.ResourceEvent(k, "PENDING", "") } - return b.WaitStackCompletion(ctx, project.Name, compose.StackCreate, w) + return b.WaitStackCompletion(ctx, project.Name, operation, w) } func (b Backend) GetVPC(ctx context.Context, project *types.Project) (string, error) { diff --git a/pkg/amazon/sdk/api.go b/pkg/amazon/sdk/api.go index bce65b2..c56df26 100644 --- a/pkg/amazon/sdk/api.go +++ b/pkg/amazon/sdk/api.go @@ -23,6 +23,8 @@ type API interface { GetStackID(ctx context.Context, name string) (string, error) WaitStackComplete(ctx context.Context, name string, operation int) error DescribeStackEvents(ctx context.Context, stackID string) ([]*cf.StackEvent, error) + CreateChangeSet(ctx context.Context, name string, template *cloudformation.Template, parameters map[string]string) (string, error) + UpdateStack(ctx context.Context, changeset string) error DescribeServices(ctx context.Context, cluster string, arns []string) ([]compose.ServiceStatus, error) diff --git a/pkg/amazon/sdk/sdk.go b/pkg/amazon/sdk/sdk.go index 2af1f78..b2d0cb1 100644 --- a/pkg/amazon/sdk/sdk.go +++ b/pkg/amazon/sdk/sdk.go @@ -175,9 +175,8 @@ func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template param := []*cloudformation.Parameter{} for name, value := range parameters { param = append(param, &cloudformation.Parameter{ - ParameterKey: aws.String(name), - ParameterValue: aws.String(value), - UsePreviousValue: aws.Bool(true), + ParameterKey: aws.String(name), + ParameterValue: aws.String(value), }) } @@ -194,6 +193,60 @@ func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template return err } +func (s sdk) CreateChangeSet(ctx context.Context, name string, template *cf.Template, parameters map[string]string) (string, error) { + logrus.Debug("Create CloudFormation Changeset") + json, err := template.JSON() + if err != nil { + return "", err + } + + param := []*cloudformation.Parameter{} + for name := range parameters { + param = append(param, &cloudformation.Parameter{ + ParameterKey: aws.String(name), + UsePreviousValue: aws.Bool(true), + }) + } + + update := fmt.Sprintf("Update%s", time.Now().Format("2006-01-02-15-04-05")) + changeset, err := s.CF.CreateChangeSetWithContext(ctx, &cloudformation.CreateChangeSetInput{ + ChangeSetName: aws.String(update), + ChangeSetType: aws.String(cloudformation.ChangeSetTypeUpdate), + StackName: aws.String(name), + TemplateBody: aws.String(string(json)), + Parameters: param, + Capabilities: []*string{ + aws.String(cloudformation.CapabilityCapabilityIam), + }, + }) + if err != nil { + return "", err + } + + err = s.CF.WaitUntilChangeSetCreateCompleteWithContext(ctx, &cloudformation.DescribeChangeSetInput{ + ChangeSetName: changeset.Id, + }) + return *changeset.Id, err +} + +func (s sdk) UpdateStack(ctx context.Context, changeset string) error { + desc, err := s.CF.DescribeChangeSetWithContext(ctx, &cloudformation.DescribeChangeSetInput{ + ChangeSetName: aws.String(changeset), + }) + if err != nil { + return err + } + + if strings.HasPrefix(aws.StringValue(desc.StatusReason), "The submitted information didn't contain changes.") { + return nil + } + + _, err = s.CF.ExecuteChangeSet(&cloudformation.ExecuteChangeSetInput{ + ChangeSetName: aws.String(changeset), + }) + return err +} + func (s sdk) WaitStackComplete(ctx context.Context, name string, operation int) error { input := &cloudformation.DescribeStacksInput{ StackName: aws.String(name), diff --git a/pkg/compose/types.go b/pkg/compose/types.go index 370bfc4..ec59578 100644 --- a/pkg/compose/types.go +++ b/pkg/compose/types.go @@ -19,6 +19,7 @@ type ServiceStatus struct { const ( StackCreate = iota + StackUpdate StackDelete )