|
| 1 | +package resourceoperation |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + "io/ioutil" |
| 7 | + "strings" |
| 8 | + "time" |
| 9 | + |
| 10 | + "github.com/aws/aws-sdk-go/aws" |
| 11 | + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" |
| 12 | + "github.com/aws/aws-sdk-go/aws/session" |
| 13 | + "github.com/aws/aws-sdk-go/service/cloudformation" |
| 14 | + "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface" |
| 15 | + "github.com/samsarahq/go/oops" |
| 16 | + "github.com/santiago-labs/telophasecli/cmd/runner" |
| 17 | + "github.com/santiago-labs/telophasecli/lib/awssess" |
| 18 | + "github.com/santiago-labs/telophasecli/resource" |
| 19 | +) |
| 20 | + |
| 21 | +type cloudformationOp struct { |
| 22 | + Account *resource.Account |
| 23 | + Operation int |
| 24 | + Stack resource.Stack |
| 25 | + OutputUI runner.ConsoleUI |
| 26 | + DependentOperations []ResourceOperation |
| 27 | + CloudformationClient cloudformationiface.CloudFormationAPI |
| 28 | +} |
| 29 | + |
| 30 | +func NewCloudformationOperation(consoleUI runner.ConsoleUI, acct *resource.Account, stack resource.Stack, op int) ResourceOperation { |
| 31 | + cfg := aws.NewConfig() |
| 32 | + if stack.Region != "" { |
| 33 | + cfg.WithRegion(stack.Region) |
| 34 | + } |
| 35 | + sess := session.Must(awssess.DefaultSession(cfg)) |
| 36 | + creds := stscreds.NewCredentials(sess, *stack.RoleARN(*acct)) |
| 37 | + cloudformationClient := cloudformation.New(sess, |
| 38 | + &aws.Config{ |
| 39 | + Credentials: creds, |
| 40 | + }) |
| 41 | + |
| 42 | + return &cloudformationOp{ |
| 43 | + Account: acct, |
| 44 | + Operation: op, |
| 45 | + Stack: stack, |
| 46 | + OutputUI: consoleUI, |
| 47 | + CloudformationClient: cloudformationClient, |
| 48 | + } |
| 49 | +} |
| 50 | + |
| 51 | +func (co *cloudformationOp) AddDependent(op ResourceOperation) { |
| 52 | + co.DependentOperations = append(co.DependentOperations, op) |
| 53 | +} |
| 54 | + |
| 55 | +func (co *cloudformationOp) ListDependents() []ResourceOperation { |
| 56 | + return co.DependentOperations |
| 57 | +} |
| 58 | + |
| 59 | +func (co *cloudformationOp) Call(ctx context.Context) error { |
| 60 | + co.OutputUI.Print(fmt.Sprintf("Executing Cloudformation stack in %s", co.Stack.Path), *co.Account) |
| 61 | + |
| 62 | + cs, err := co.createChangeSet(ctx) |
| 63 | + if err != nil { |
| 64 | + return err |
| 65 | + } |
| 66 | + if aws.StringValue(cs.Status) == cloudformation.ChangeSetStatusFailed { |
| 67 | + if strings.Contains(aws.StringValue(cs.StatusReason), "The submitted information didn't contain changes") { |
| 68 | + co.OutputUI.Print(fmt.Sprintf("change set (%s) resulted in no diff, skipping", *co.Stack.ChangeSetName()), *co.Account) |
| 69 | + return nil |
| 70 | + } else { |
| 71 | + return oops.Errorf("change set failed, reason (%s)", aws.StringValue(cs.StatusReason)) |
| 72 | + } |
| 73 | + } else { |
| 74 | + co.OutputUI.Print("Created change set with changes:"+cs.String(), *co.Account) |
| 75 | + } |
| 76 | + |
| 77 | + // End call if we aren't deploying |
| 78 | + if co.Operation != Deploy { |
| 79 | + return nil |
| 80 | + } |
| 81 | + |
| 82 | + _, err = co.executeChangeSet(ctx, cs.ChangeSetId) |
| 83 | + if err != nil { |
| 84 | + return oops.Wrapf(err, "executing change set") |
| 85 | + } |
| 86 | + co.OutputUI.Print("Executed change set", *co.Account) |
| 87 | + |
| 88 | + return nil |
| 89 | +} |
| 90 | + |
| 91 | +func (co *cloudformationOp) createChangeSet(ctx context.Context) (*cloudformation.DescribeChangeSetOutput, error) { |
| 92 | + params, err := co.Stack.CloudformationParametersType() |
| 93 | + if err != nil { |
| 94 | + return nil, oops.Wrapf(err, "CloudformationParameters") |
| 95 | + } |
| 96 | + |
| 97 | + // If we can find the stack then we just update. If not then we continue on |
| 98 | + changeSetType := cloudformation.ChangeSetTypeUpdate |
| 99 | + |
| 100 | + stack, err := co.CloudformationClient.DescribeStacksWithContext(ctx, |
| 101 | + &cloudformation.DescribeStacksInput{ |
| 102 | + StackName: co.Stack.CloudformationStackName(), |
| 103 | + }) |
| 104 | + if err != nil { |
| 105 | + if strings.Contains(err.Error(), "does not exist") { |
| 106 | + changeSetType = cloudformation.ChangeSetTypeCreate |
| 107 | + // Reset err in case it is re-referenced somewhere else |
| 108 | + err = nil |
| 109 | + } else { |
| 110 | + return nil, oops.Wrapf(err, "describe stack with name: (%s)", *co.Stack.CloudformationStackName()) |
| 111 | + } |
| 112 | + } else { |
| 113 | + if len(stack.Stacks) == 1 && aws.StringValue(stack.Stacks[0].StackStatus) == cloudformation.StackStatusReviewInProgress { |
| 114 | + // If we reset to Create the change set the same change set will be reused. |
| 115 | + changeSetType = cloudformation.ChangeSetTypeCreate |
| 116 | + } |
| 117 | + } |
| 118 | + template, err := ioutil.ReadFile(co.Stack.Path) |
| 119 | + if err != nil { |
| 120 | + return nil, fmt.Errorf("failed to read stack template at path: (%s) should be a path to one file", co.Stack.Path) |
| 121 | + } |
| 122 | + |
| 123 | + strTemplate := string(template) |
| 124 | + changeSet, err := co.CloudformationClient.CreateChangeSetWithContext(ctx, |
| 125 | + &cloudformation.CreateChangeSetInput{ |
| 126 | + Parameters: params, |
| 127 | + StackName: co.Stack.CloudformationStackName(), |
| 128 | + ChangeSetName: co.Stack.ChangeSetName(), |
| 129 | + TemplateBody: &strTemplate, |
| 130 | + ChangeSetType: &changeSetType, |
| 131 | + }, |
| 132 | + ) |
| 133 | + if err != nil { |
| 134 | + return nil, oops.Wrapf(err, "createChangeSet for stack: %s", co.Stack.Name) |
| 135 | + } |
| 136 | + |
| 137 | + for { |
| 138 | + cs, err := co.CloudformationClient.DescribeChangeSetWithContext(ctx, |
| 139 | + &cloudformation.DescribeChangeSetInput{ |
| 140 | + StackName: changeSet.StackId, |
| 141 | + ChangeSetName: changeSet.Id, |
| 142 | + }) |
| 143 | + if err != nil { |
| 144 | + return nil, oops.Wrapf(err, "DescribeChangeSet") |
| 145 | + } |
| 146 | + |
| 147 | + state := aws.StringValue(cs.Status) |
| 148 | + switch state { |
| 149 | + case cloudformation.ChangeSetStatusCreateInProgress: |
| 150 | + co.OutputUI.Print(fmt.Sprintf("Still creating change set for stack: %s", *co.Stack.CloudformationStackName()), *co.Account) |
| 151 | + |
| 152 | + case cloudformation.ChangeSetStatusCreateComplete: |
| 153 | + co.OutputUI.Print(fmt.Sprintf("Successfully created change set for stack: %s", *co.Stack.CloudformationStackName()), *co.Account) |
| 154 | + return cs, nil |
| 155 | + |
| 156 | + case cloudformation.ChangeSetStatusFailed: |
| 157 | + return cs, nil |
| 158 | + } |
| 159 | + |
| 160 | + time.Sleep(5 * time.Second) |
| 161 | + } |
| 162 | +} |
| 163 | + |
| 164 | +func (co *cloudformationOp) executeChangeSet(ctx context.Context, changeSetID *string) (*cloudformation.DescribeChangeSetOutput, error) { |
| 165 | + _, err := co.CloudformationClient.ExecuteChangeSetWithContext(ctx, |
| 166 | + &cloudformation.ExecuteChangeSetInput{ |
| 167 | + ChangeSetName: changeSetID, |
| 168 | + }) |
| 169 | + if err != nil { |
| 170 | + return nil, oops.Wrapf(err, "executing change set") |
| 171 | + } |
| 172 | + |
| 173 | + for { |
| 174 | + cs, err := co.CloudformationClient.DescribeChangeSetWithContext(ctx, |
| 175 | + &cloudformation.DescribeChangeSetInput{ |
| 176 | + ChangeSetName: changeSetID, |
| 177 | + }) |
| 178 | + if err != nil { |
| 179 | + return nil, oops.Wrapf(err, "DescribeChangeSet") |
| 180 | + } |
| 181 | + |
| 182 | + state := aws.StringValue(cs.ExecutionStatus) |
| 183 | + switch state { |
| 184 | + case cloudformation.ExecutionStatusExecuteInProgress: |
| 185 | + co.OutputUI.Print(fmt.Sprintf("Still executing change set for stack: (%s) for path: %s", *co.Stack.CloudformationStackName(), co.Stack.Path), *co.Account) |
| 186 | + |
| 187 | + case cloudformation.ExecutionStatusExecuteComplete: |
| 188 | + co.OutputUI.Print(fmt.Sprintf("Successfully executed change set for stack: (%s) for path: %s", *co.Stack.CloudformationStackName(), co.Stack.Path), *co.Account) |
| 189 | + return cs, nil |
| 190 | + |
| 191 | + case cloudformation.ExecutionStatusExecuteFailed: |
| 192 | + co.OutputUI.Print(fmt.Sprintf("Failed to execute change set: (%s) for path: %s", *co.Stack.CloudformationStackName(), co.Stack.Path), *co.Account) |
| 193 | + return cs, oops.Errorf("ExecuteChangeSet failed") |
| 194 | + } |
| 195 | + |
| 196 | + time.Sleep(5 * time.Second) |
| 197 | + } |
| 198 | +} |
| 199 | + |
| 200 | +func (co *cloudformationOp) ToString() string { |
| 201 | + return "" |
| 202 | +} |
0 commit comments