Skip to content
This repository was archived by the owner on Sep 30, 2020. It is now read-only.

Commit 713ffd2

Browse files
authored
Merge pull request #104 from mumoshu/extract-cfn
refactoring: Extract CloudFormation stack provisioning into its own package
2 parents c413d9d + 8113e31 commit 713ffd2

File tree

4 files changed

+365
-302
lines changed

4 files changed

+365
-302
lines changed

cfnstack/cfnstack.go

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package cfnstack
2+
3+
import (
4+
"github.com/aws/aws-sdk-go/aws"
5+
"github.com/aws/aws-sdk-go/service/cloudformation"
6+
"github.com/aws/aws-sdk-go/service/s3"
7+
"strings"
8+
)
9+
10+
var CFN_TEMPLATE_SIZE_LIMIT = 51200
11+
12+
type CreationService interface {
13+
CreateStack(*cloudformation.CreateStackInput) (*cloudformation.CreateStackOutput, error)
14+
}
15+
16+
type UpdateService interface {
17+
UpdateStack(input *cloudformation.UpdateStackInput) (*cloudformation.UpdateStackOutput, error)
18+
}
19+
20+
type CRUDService interface {
21+
CreateStack(*cloudformation.CreateStackInput) (*cloudformation.CreateStackOutput, error)
22+
UpdateStack(input *cloudformation.UpdateStackInput) (*cloudformation.UpdateStackOutput, error)
23+
DescribeStacks(input *cloudformation.DescribeStacksInput) (*cloudformation.DescribeStacksOutput, error)
24+
DescribeStackEvents(input *cloudformation.DescribeStackEventsInput) (*cloudformation.DescribeStackEventsOutput, error)
25+
}
26+
27+
type S3ObjectPutterService interface {
28+
PutObject(input *s3.PutObjectInput) (*s3.PutObjectOutput, error)
29+
}
30+
31+
func StackEventErrMsgs(events []*cloudformation.StackEvent) []string {
32+
var errMsgs []string
33+
34+
for _, event := range events {
35+
if aws.StringValue(event.ResourceStatus) == cloudformation.ResourceStatusCreateFailed {
36+
// Only show actual failures, not cancelled dependent resources.
37+
if aws.StringValue(event.ResourceStatusReason) != "Resource creation cancelled" {
38+
errMsgs = append(errMsgs,
39+
strings.TrimSpace(
40+
strings.Join([]string{
41+
aws.StringValue(event.ResourceStatus),
42+
aws.StringValue(event.ResourceType),
43+
aws.StringValue(event.LogicalResourceId),
44+
aws.StringValue(event.ResourceStatusReason),
45+
}, " ")))
46+
}
47+
}
48+
}
49+
50+
return errMsgs
51+
}

cfnstack/provisioner.go

+283
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
package cfnstack
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"github.com/aws/aws-sdk-go/aws"
7+
"github.com/aws/aws-sdk-go/aws/session"
8+
"github.com/aws/aws-sdk-go/service/cloudformation"
9+
"github.com/aws/aws-sdk-go/service/s3"
10+
"regexp"
11+
"strings"
12+
"time"
13+
)
14+
15+
type Provisioner struct {
16+
stackName string
17+
stackTags map[string]string
18+
stackPolicyBody string
19+
session *session.Session
20+
}
21+
22+
func NewProvisioner(name string, stackTags map[string]string, stackPolicyBody string, session *session.Session) *Provisioner {
23+
return &Provisioner{
24+
stackName: name,
25+
stackTags: stackTags,
26+
stackPolicyBody: stackPolicyBody,
27+
session: session,
28+
}
29+
}
30+
31+
func (c *Provisioner) UploadTemplate(s3Svc S3ObjectPutterService, s3URI string, stackBody string) (string, error) {
32+
re := regexp.MustCompile("s3://(?P<bucket>[^/]+)/(?P<directory>.+[^/])/*$")
33+
matches := re.FindStringSubmatch(s3URI)
34+
35+
var bucket string
36+
var key string
37+
if len(matches) == 3 {
38+
directory := matches[2]
39+
40+
bucket = matches[1]
41+
key = fmt.Sprintf("%s/%s/stack.json", directory, c.stackName)
42+
} else {
43+
re := regexp.MustCompile("s3://(?P<bucket>[^/]+)/*$")
44+
matches := re.FindStringSubmatch(s3URI)
45+
46+
if len(matches) == 2 {
47+
bucket = matches[1]
48+
key = fmt.Sprintf("%s/stack.json", c.stackName)
49+
} else {
50+
return "", fmt.Errorf("failed to parse s3 uri(=%s): The valid uri pattern for it is s3://mybucket/mydir or s3://mybucket", s3URI)
51+
}
52+
}
53+
54+
contentLength := int64(len(stackBody))
55+
body := strings.NewReader(stackBody)
56+
57+
_, err := s3Svc.PutObject(&s3.PutObjectInput{
58+
Bucket: aws.String(bucket),
59+
Key: aws.String(key),
60+
Body: body,
61+
ContentLength: aws.Int64(contentLength),
62+
ContentType: aws.String("application/json"),
63+
})
64+
65+
if err != nil {
66+
return "", err
67+
}
68+
69+
templateURL := fmt.Sprintf("https://s3.amazonaws.com/%s/%s", bucket, key)
70+
71+
return templateURL, nil
72+
}
73+
74+
func (c *Provisioner) uploadTemplateIfNecessary(s3Svc S3ObjectPutterService, stackBody string, s3URI string) (*string, error) {
75+
if len(stackBody) > CFN_TEMPLATE_SIZE_LIMIT {
76+
if s3URI == "" {
77+
return nil, fmt.Errorf("stack template's size(=%d) exceeds the 51200 bytes limit of cloudformation. `--s3-uri s3://<bucket>/path/to/dir` must be specified to upload it to S3 beforehand", len(stackBody))
78+
}
79+
80+
templateURL, err := c.UploadTemplate(s3Svc, s3URI, stackBody)
81+
if err != nil {
82+
return nil, fmt.Errorf("Template upload failed: %v", err)
83+
}
84+
85+
return &templateURL, nil
86+
}
87+
88+
return nil, nil
89+
}
90+
91+
func (c *Provisioner) CreateStack(cfSvc CreationService, s3Svc S3ObjectPutterService, stackBody string, s3URI string) (*cloudformation.CreateStackOutput, error) {
92+
templateURL, uploadErr := c.uploadTemplateIfNecessary(s3Svc, stackBody, s3URI)
93+
94+
if uploadErr != nil {
95+
return nil, fmt.Errorf("template upload failed: %v", uploadErr)
96+
} else if templateURL != nil {
97+
resp, err := c.createStackFromTemplateURL(cfSvc, *templateURL)
98+
if err != nil {
99+
return nil, fmt.Errorf("stack creation failed: %v", err)
100+
}
101+
102+
return resp, nil
103+
} else {
104+
resp, err := c.CreateStackFromTemplateBody(cfSvc, stackBody)
105+
if err != nil {
106+
return nil, fmt.Errorf("stack creation failed: %v", err)
107+
}
108+
109+
return resp, nil
110+
}
111+
}
112+
113+
func (c *Provisioner) CreateStackAndWait(cfSvc CRUDService, s3Svc S3ObjectPutterService, stackBody string, s3URI string) error {
114+
resp, err := c.CreateStack(cfSvc, s3Svc, stackBody, s3URI)
115+
if err != nil {
116+
return err
117+
}
118+
119+
req := cloudformation.DescribeStacksInput{
120+
StackName: resp.StackId,
121+
}
122+
123+
for {
124+
resp, err := cfSvc.DescribeStacks(&req)
125+
if err != nil {
126+
return err
127+
}
128+
if len(resp.Stacks) == 0 {
129+
return fmt.Errorf("stack not found")
130+
}
131+
statusString := aws.StringValue(resp.Stacks[0].StackStatus)
132+
switch statusString {
133+
case cloudformation.ResourceStatusCreateComplete:
134+
return nil
135+
case cloudformation.ResourceStatusCreateFailed:
136+
errMsg := fmt.Sprintf(
137+
"Stack creation failed: %s : %s",
138+
statusString,
139+
aws.StringValue(resp.Stacks[0].StackStatusReason),
140+
)
141+
errMsg = errMsg + "\n\nPrinting the most recent failed stack events:\n"
142+
143+
stackEventsOutput, err := cfSvc.DescribeStackEvents(
144+
&cloudformation.DescribeStackEventsInput{
145+
StackName: resp.Stacks[0].StackName,
146+
})
147+
if err != nil {
148+
return err
149+
}
150+
errMsg = errMsg + strings.Join(StackEventErrMsgs(stackEventsOutput.StackEvents), "\n")
151+
return errors.New(errMsg)
152+
case cloudformation.ResourceStatusCreateInProgress:
153+
time.Sleep(3 * time.Second)
154+
continue
155+
default:
156+
return fmt.Errorf("unexpected stack status: %s", statusString)
157+
}
158+
}
159+
}
160+
161+
func (c *Provisioner) baseCreateStackInput() *cloudformation.CreateStackInput {
162+
var tags []*cloudformation.Tag
163+
for k, v := range c.stackTags {
164+
key := k
165+
value := v
166+
tags = append(tags, &cloudformation.Tag{Key: &key, Value: &value})
167+
}
168+
169+
return &cloudformation.CreateStackInput{
170+
StackName: aws.String(c.stackName),
171+
OnFailure: aws.String(cloudformation.OnFailureDoNothing),
172+
Capabilities: []*string{aws.String(cloudformation.CapabilityCapabilityIam)},
173+
Tags: tags,
174+
StackPolicyBody: aws.String(c.stackPolicyBody),
175+
}
176+
}
177+
178+
func (c *Provisioner) CreateStackFromTemplateBody(cfSvc CreationService, stackBody string) (*cloudformation.CreateStackOutput, error) {
179+
input := c.baseCreateStackInput()
180+
input.TemplateBody = &stackBody
181+
return cfSvc.CreateStack(input)
182+
}
183+
184+
func (c *Provisioner) createStackFromTemplateURL(cfSvc CreationService, stackTemplateURL string) (*cloudformation.CreateStackOutput, error) {
185+
input := c.baseCreateStackInput()
186+
input.TemplateURL = &stackTemplateURL
187+
return cfSvc.CreateStack(input)
188+
}
189+
190+
func (c *Provisioner) baseUpdateStackInput() *cloudformation.UpdateStackInput {
191+
return &cloudformation.UpdateStackInput{
192+
Capabilities: []*string{aws.String(cloudformation.CapabilityCapabilityIam)},
193+
StackName: aws.String(c.stackName),
194+
}
195+
}
196+
197+
func (c *Provisioner) updateStackWithTemplateBody(cfSvc UpdateService, stackBody string) (*cloudformation.UpdateStackOutput, error) {
198+
input := c.baseUpdateStackInput()
199+
input.TemplateBody = aws.String(stackBody)
200+
return cfSvc.UpdateStack(input)
201+
}
202+
203+
func (c *Provisioner) updateStackWithTemplateURL(cfSvc UpdateService, templateURL string) (*cloudformation.UpdateStackOutput, error) {
204+
input := c.baseUpdateStackInput()
205+
input.TemplateURL = aws.String(templateURL)
206+
return cfSvc.UpdateStack(input)
207+
}
208+
209+
func (c *Provisioner) UpdateStack(cfSvc UpdateService, s3Svc S3ObjectPutterService, stackBody string, s3URI string) (*cloudformation.UpdateStackOutput, error) {
210+
templateURL, uploadErr := c.uploadTemplateIfNecessary(s3Svc, stackBody, s3URI)
211+
212+
if uploadErr != nil {
213+
return nil, fmt.Errorf("template upload failed: %v", uploadErr)
214+
} else if templateURL != nil {
215+
resp, err := c.updateStackWithTemplateURL(cfSvc, *templateURL)
216+
if err != nil {
217+
return nil, fmt.Errorf("stack update failed: %v", err)
218+
}
219+
220+
return resp, nil
221+
} else {
222+
resp, err := c.updateStackWithTemplateBody(cfSvc, stackBody)
223+
if err != nil {
224+
return nil, fmt.Errorf("stack update failed: %v", err)
225+
}
226+
227+
return resp, nil
228+
}
229+
}
230+
231+
func (c *Provisioner) UpdateStackAndWait(cfSvc CRUDService, s3Svc S3ObjectPutterService, stackBody string, s3URI string) (string, error) {
232+
updateOutput, err := c.UpdateStack(cfSvc, s3Svc, stackBody, s3URI)
233+
if err != nil {
234+
return "", fmt.Errorf("error updating cloudformation stack: %v", err)
235+
}
236+
req := cloudformation.DescribeStacksInput{
237+
StackName: updateOutput.StackId,
238+
}
239+
for {
240+
resp, err := cfSvc.DescribeStacks(&req)
241+
if err != nil {
242+
return "", err
243+
}
244+
if len(resp.Stacks) == 0 {
245+
return "", fmt.Errorf("stack not found")
246+
}
247+
statusString := aws.StringValue(resp.Stacks[0].StackStatus)
248+
switch statusString {
249+
case cloudformation.ResourceStatusUpdateComplete:
250+
return updateOutput.String(), nil
251+
case cloudformation.ResourceStatusUpdateFailed, cloudformation.StackStatusUpdateRollbackComplete, cloudformation.StackStatusUpdateRollbackFailed:
252+
errMsg := fmt.Sprintf("Stack status: %s : %s", statusString, aws.StringValue(resp.Stacks[0].StackStatusReason))
253+
return "", errors.New(errMsg)
254+
case cloudformation.ResourceStatusUpdateInProgress, cloudformation.StackStatusUpdateCompleteCleanupInProgress:
255+
time.Sleep(3 * time.Second)
256+
continue
257+
default:
258+
return "", fmt.Errorf("unexpected stack status: %s", statusString)
259+
}
260+
}
261+
}
262+
263+
func (c *Provisioner) Validate(stackBody string, s3URI string) (string, error) {
264+
validateInput := cloudformation.ValidateTemplateInput{}
265+
266+
templateURL, uploadErr := c.uploadTemplateIfNecessary(s3.New(c.session), stackBody, s3URI)
267+
268+
if uploadErr != nil {
269+
return "", fmt.Errorf("template upload failed: %v", uploadErr)
270+
} else if templateURL != nil {
271+
validateInput.TemplateURL = templateURL
272+
} else {
273+
validateInput.TemplateBody = aws.String(stackBody)
274+
}
275+
276+
cfSvc := cloudformation.New(c.session)
277+
validationReport, err := cfSvc.ValidateTemplate(&validateInput)
278+
if err != nil {
279+
return "", fmt.Errorf("invalid cloudformation stack: %v", err)
280+
}
281+
282+
return validationReport.String(), nil
283+
}

0 commit comments

Comments
 (0)