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

Commit 6b9d280

Browse files
authored
feat: `kube-aws up --s3-uri s3://<bucket>/<directory> to automatically avoid the 51200 bytes limitation errors of CloudFormation (#45)
feat: Add the `--s3-uri s3://<bucket>/<directory` flag to automatically avoid the 51200 bytes limitation errors of CloudFormation If you are hit by the cloudformation limit, `kube-aws up`, `kube-aws update`, `kube-aws validate` now fail with a specific error explaining the limit and how to work-around it: ``` $ kube-aws up Creating AWS resources. This should take around 5 minutes. Error: Error creating cluster: stack-template.json size(=51673) exceeds the 51200 bytes limit of cloudformation. `--s3-uri s3://<bucket>/path/to/dir` must be specified to upload it to S3 beforehand ... As the error message says, if you provide the `--s3-uri` option composed of a s3 bucket's name and a path to directory, `kube-aws` uploads your template to s3 if necessary: ``` $ kube-aws up --s3-uri s3://mybucket/mydir ``` Also note that `--s3-uri` accepts URIs without directories, too. Both `--s3-uri s3://mybucket` and `s3-uri s3://mybucket/mydir` are valid. The E2E test script now include the cluster updating test with `kube-aws update`. resolves #38 (for now)
1 parent a346f9f commit 6b9d280

File tree

6 files changed

+325
-28
lines changed

6 files changed

+325
-28
lines changed

cluster/cluster.go

+165-17
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ import (
1818
"github.com/aws/aws-sdk-go/service/elb"
1919
"github.com/aws/aws-sdk-go/service/route53"
2020

21+
"github.com/aws/aws-sdk-go/service/s3"
2122
"github.com/coreos/kube-aws/config"
2223
)
2324

2425
// VERSION set by build script
2526
var VERSION = "UNKNOWN"
2627

28+
var CFN_TEMPLATE_SIZE_LIMIT = 51200
29+
2730
type Info struct {
2831
Name string
2932
ControllerHost string
@@ -61,9 +64,34 @@ type Cluster struct {
6164
session *session.Session
6265
}
6366

64-
func (c *Cluster) ValidateStack(stackBody string) (string, error) {
65-
validateInput := cloudformation.ValidateTemplateInput{
66-
TemplateBody: &stackBody,
67+
func (c *Cluster) uploadTemplateIfNecessary(s3Svc s3ObjectPutterService, stackBody string, s3URI string) (*string, error) {
68+
if len(stackBody) > CFN_TEMPLATE_SIZE_LIMIT {
69+
if s3URI == "" {
70+
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))
71+
}
72+
73+
templateURL, err := c.uploadTemplate(s3Svc, s3URI, stackBody)
74+
if err != nil {
75+
return nil, fmt.Errorf("Template upload failed: %v", err)
76+
}
77+
78+
return &templateURL, nil
79+
}
80+
81+
return nil, nil
82+
}
83+
84+
func (c *Cluster) ValidateStack(stackBody string, s3URI string) (string, error) {
85+
validateInput := cloudformation.ValidateTemplateInput{}
86+
87+
templateURL, uploadErr := c.uploadTemplateIfNecessary(s3.New(c.session), stackBody, s3URI)
88+
89+
if uploadErr != nil {
90+
return "", fmt.Errorf("template upload failed: %v", uploadErr)
91+
} else if templateURL != nil {
92+
validateInput.TemplateURL = templateURL
93+
} else {
94+
validateInput.TemplateBody = aws.String(stackBody)
6795
}
6896

6997
cfSvc := cloudformation.New(c.session)
@@ -140,7 +168,29 @@ func (c *Cluster) validateExistingVPCState(ec2Svc ec2Service) error {
140168
return nil
141169
}
142170

143-
func (c *Cluster) Create(stackBody string) error {
171+
func (c *Cluster) createStack(cfSvc *cloudformation.CloudFormation, s3Svc s3ObjectPutterService, stackBody string, s3URI string) (*cloudformation.CreateStackOutput, error) {
172+
templateURL, uploadErr := c.uploadTemplateIfNecessary(s3Svc, stackBody, s3URI)
173+
174+
if uploadErr != nil {
175+
return nil, fmt.Errorf("template upload failed: %v", uploadErr)
176+
} else if templateURL != nil {
177+
resp, err := c.createStackFromTemplateURL(cfSvc, *templateURL)
178+
if err != nil {
179+
return nil, fmt.Errorf("stack creation failed: %v", err)
180+
}
181+
182+
return resp, nil
183+
} else {
184+
resp, err := c.createStackFromTemplateBody(cfSvc, stackBody)
185+
if err != nil {
186+
return nil, fmt.Errorf("stack creation failed: %v", err)
187+
}
188+
189+
return resp, nil
190+
}
191+
}
192+
193+
func (c *Cluster) Create(stackBody string, s3URI string) error {
144194
r53Svc := route53.New(c.session)
145195
if err := c.validateDNSConfig(r53Svc); err != nil {
146196
return err
@@ -164,7 +214,9 @@ func (c *Cluster) Create(stackBody string) error {
164214
}
165215

166216
cfSvc := cloudformation.New(c.session)
167-
resp, err := c.createStack(cfSvc, stackBody)
217+
s3Svc := s3.New(c.session)
218+
219+
resp, err := c.createStack(cfSvc, s3Svc, stackBody, s3URI)
168220
if err != nil {
169221
return err
170222
}
@@ -211,24 +263,73 @@ func (c *Cluster) Create(stackBody string) error {
211263
}
212264
}
213265

214-
type cloudformationService interface {
266+
type cloudformationStackCreationService interface {
215267
CreateStack(*cloudformation.CreateStackInput) (*cloudformation.CreateStackOutput, error)
216268
}
217269

218-
func (c *Cluster) createStack(cfSvc cloudformationService, stackBody string) (*cloudformation.CreateStackOutput, error) {
270+
type cloudformationStackUpdateService interface {
271+
UpdateStack(input *cloudformation.UpdateStackInput) (*cloudformation.UpdateStackOutput, error)
272+
}
273+
274+
type s3ObjectPutterService interface {
275+
PutObject(input *s3.PutObjectInput) (*s3.PutObjectOutput, error)
276+
}
277+
278+
func (c *Cluster) uploadTemplate(s3Svc s3ObjectPutterService, s3URI string, stackBody string) (string, error) {
279+
re := regexp.MustCompile("s3://(?P<bucket>[^/]+)/(?P<directory>.+[^/])/*$")
280+
matches := re.FindStringSubmatch(s3URI)
281+
282+
var bucket string
283+
var key string
284+
if len(matches) == 3 {
285+
directory := matches[2]
286+
287+
bucket = matches[1]
288+
key = fmt.Sprintf("%s/%s/stack.json", directory, c.ClusterName)
289+
} else {
290+
re := regexp.MustCompile("s3://(?P<bucket>[^/]+)/*$")
291+
matches := re.FindStringSubmatch(s3URI)
292+
293+
if len(matches) == 2 {
294+
bucket = matches[1]
295+
key = fmt.Sprintf("%s/stack.json", c.ClusterName)
296+
} else {
297+
return "", fmt.Errorf("failed to parse s3 uri(=%s): The valid uri pattern for it is s3://mybucket/mydir or s3://mybucket", s3URI)
298+
}
299+
}
300+
301+
contentLength := int64(len(stackBody))
302+
body := strings.NewReader(stackBody)
303+
304+
_, err := s3Svc.PutObject(&s3.PutObjectInput{
305+
Bucket: aws.String(bucket),
306+
Key: aws.String(key),
307+
Body: body,
308+
ContentLength: aws.Int64(contentLength),
309+
ContentType: aws.String("application/json"),
310+
})
311+
312+
if err != nil {
313+
return "", err
314+
}
315+
316+
templateURL := fmt.Sprintf("https://s3.amazonaws.com/%s/%s", bucket, key)
317+
318+
return templateURL, nil
319+
}
219320

321+
func (c *Cluster) baseCreateStackInput() *cloudformation.CreateStackInput {
220322
var tags []*cloudformation.Tag
221323
for k, v := range c.StackTags {
222324
key := k
223325
value := v
224326
tags = append(tags, &cloudformation.Tag{Key: &key, Value: &value})
225327
}
226328

227-
creq := &cloudformation.CreateStackInput{
329+
return &cloudformation.CreateStackInput{
228330
StackName: aws.String(c.ClusterName),
229331
OnFailure: aws.String(cloudformation.OnFailureDoNothing),
230332
Capabilities: []*string{aws.String(cloudformation.CapabilityCapabilityIam)},
231-
TemplateBody: &stackBody,
232333
Tags: tags,
233334
StackPolicyBody: aws.String(`{
234335
"Statement" : [
@@ -248,8 +349,18 @@ func (c *Cluster) createStack(cfSvc cloudformationService, stackBody string) (*c
248349
}
249350
`),
250351
}
352+
}
251353

252-
return cfSvc.CreateStack(creq)
354+
func (c *Cluster) createStackFromTemplateBody(cfSvc cloudformationStackCreationService, stackBody string) (*cloudformation.CreateStackOutput, error) {
355+
input := c.baseCreateStackInput()
356+
input.TemplateBody = &stackBody
357+
return cfSvc.CreateStack(input)
358+
}
359+
360+
func (c *Cluster) createStackFromTemplateURL(cfSvc cloudformationStackCreationService, stackTemplateURL string) (*cloudformation.CreateStackOutput, error) {
361+
input := c.baseCreateStackInput()
362+
input.TemplateURL = &stackTemplateURL
363+
return cfSvc.CreateStack(input)
253364
}
254365

255366
/*
@@ -320,20 +431,57 @@ func (c *Cluster) lockEtcdResources(cfSvc *cloudformation.CloudFormation, stackB
320431
return buf.String(), nil
321432
}
322433

323-
func (c *Cluster) Update(stackBody string) (string, error) {
434+
func (c *Cluster) baseUpdateStackInput() *cloudformation.UpdateStackInput {
435+
return &cloudformation.UpdateStackInput{
436+
Capabilities: []*string{aws.String(cloudformation.CapabilityCapabilityIam)},
437+
StackName: aws.String(c.ClusterName),
438+
}
439+
}
440+
441+
func (c *Cluster) updateStackWithTemplateBody(cfSvc cloudformationStackUpdateService, stackBody string) (*cloudformation.UpdateStackOutput, error) {
442+
input := c.baseUpdateStackInput()
443+
input.TemplateBody = aws.String(stackBody)
444+
return cfSvc.UpdateStack(input)
445+
}
446+
447+
func (c *Cluster) updateStackWithTemplateURL(cfSvc cloudformationStackUpdateService, templateURL string) (*cloudformation.UpdateStackOutput, error) {
448+
input := c.baseUpdateStackInput()
449+
input.TemplateURL = aws.String(templateURL)
450+
return cfSvc.UpdateStack(input)
451+
}
452+
453+
func (c *Cluster) updateStack(cfSvc cloudformationStackUpdateService, s3Svc s3ObjectPutterService, stackBody string, s3URI string) (*cloudformation.UpdateStackOutput, error) {
454+
templateURL, uploadErr := c.uploadTemplateIfNecessary(s3Svc, stackBody, s3URI)
455+
456+
if uploadErr != nil {
457+
return nil, fmt.Errorf("template upload failed: %v", uploadErr)
458+
} else if templateURL != nil {
459+
resp, err := c.updateStackWithTemplateURL(cfSvc, *templateURL)
460+
if err != nil {
461+
return nil, fmt.Errorf("stack update failed: %v", err)
462+
}
463+
464+
return resp, nil
465+
} else {
466+
resp, err := c.updateStackWithTemplateBody(cfSvc, stackBody)
467+
if err != nil {
468+
return nil, fmt.Errorf("stack update failed: %v", err)
469+
}
470+
471+
return resp, nil
472+
}
473+
}
324474

475+
func (c *Cluster) Update(stackBody string, s3URI string) (string, error) {
325476
cfSvc := cloudformation.New(c.session)
477+
s3Svc := s3.New(c.session)
478+
326479
var err error
327480
if stackBody, err = c.lockEtcdResources(cfSvc, stackBody); err != nil {
328481
return "", err
329482
}
330-
input := &cloudformation.UpdateStackInput{
331-
Capabilities: []*string{aws.String(cloudformation.CapabilityCapabilityIam)},
332-
StackName: aws.String(c.ClusterName),
333-
TemplateBody: aws.String(stackBody),
334-
}
335483

336-
updateOutput, err := cfSvc.UpdateStack(input)
484+
updateOutput, err := c.updateStack(cfSvc, s3Svc, stackBody, s3URI)
337485
if err != nil {
338486
return "", fmt.Errorf("error updating cloudformation stack: %v", err)
339487
}

0 commit comments

Comments
 (0)