Просмотр исходного кода

Merge pull request #869 from docker/s3

publish Cloudformation template on s3
Nicolas De loof 5 лет назад
Родитель
Сommit
16c3223623
6 измененных файлов с 152 добавлено и 62 удалено
  1. 2 2
      ecs/aws.go
  2. 8 8
      ecs/aws_mock.go
  3. 138 50
      ecs/sdk.go
  4. 2 2
      ecs/up.go
  5. 1 0
      go.mod
  6. 1 0
      go.sum

+ 2 - 2
ecs/aws.go

@@ -42,8 +42,8 @@ type API interface {
 	GetSubNets(ctx context.Context, vpcID string) ([]awsResource, error)
 	GetSubNets(ctx context.Context, vpcID string) ([]awsResource, error)
 	GetRoleArn(ctx context.Context, name string) (string, error)
 	GetRoleArn(ctx context.Context, name string) (string, error)
 	StackExists(ctx context.Context, name string) (bool, error)
 	StackExists(ctx context.Context, name string) (bool, error)
-	CreateStack(ctx context.Context, name string, template []byte) error
-	CreateChangeSet(ctx context.Context, name string, template []byte) (string, error)
+	CreateStack(ctx context.Context, name string, region string, template []byte) error
+	CreateChangeSet(ctx context.Context, name string, region string, template []byte) (string, error)
 	UpdateStack(ctx context.Context, changeset string) error
 	UpdateStack(ctx context.Context, changeset string) error
 	WaitStackComplete(ctx context.Context, name string, operation int) error
 	WaitStackComplete(ctx context.Context, name string, operation int) error
 	GetStackID(ctx context.Context, name string) (string, error)
 	GetStackID(ctx context.Context, name string) (string, error)

+ 8 - 8
ecs/aws_mock.go

@@ -66,18 +66,18 @@ func (mr *MockAPIMockRecorder) CheckVPC(arg0, arg1 interface{}) *gomock.Call {
 }
 }
 
 
 // CreateChangeSet mocks base method
 // CreateChangeSet mocks base method
-func (m *MockAPI) CreateChangeSet(arg0 context.Context, arg1 string, arg2 []byte) (string, error) {
+func (m *MockAPI) CreateChangeSet(arg0 context.Context, arg1, arg2 string, arg3 []byte) (string, error) {
 	m.ctrl.T.Helper()
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "CreateChangeSet", arg0, arg1, arg2)
+	ret := m.ctrl.Call(m, "CreateChangeSet", arg0, arg1, arg2, arg3)
 	ret0, _ := ret[0].(string)
 	ret0, _ := ret[0].(string)
 	ret1, _ := ret[1].(error)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 	return ret0, ret1
 }
 }
 
 
 // CreateChangeSet indicates an expected call of CreateChangeSet
 // CreateChangeSet indicates an expected call of CreateChangeSet
-func (mr *MockAPIMockRecorder) CreateChangeSet(arg0, arg1, arg2 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) CreateChangeSet(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateChangeSet", reflect.TypeOf((*MockAPI)(nil).CreateChangeSet), arg0, arg1, arg2)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateChangeSet", reflect.TypeOf((*MockAPI)(nil).CreateChangeSet), arg0, arg1, arg2, arg3)
 }
 }
 
 
 // CreateCluster mocks base method
 // CreateCluster mocks base method
@@ -126,17 +126,17 @@ func (mr *MockAPIMockRecorder) CreateSecret(arg0, arg1 interface{}) *gomock.Call
 }
 }
 
 
 // CreateStack mocks base method
 // CreateStack mocks base method
-func (m *MockAPI) CreateStack(arg0 context.Context, arg1 string, arg2 []byte) error {
+func (m *MockAPI) CreateStack(arg0 context.Context, arg1, arg2 string, arg3 []byte) error {
 	m.ctrl.T.Helper()
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "CreateStack", arg0, arg1, arg2)
+	ret := m.ctrl.Call(m, "CreateStack", arg0, arg1, arg2, arg3)
 	ret0, _ := ret[0].(error)
 	ret0, _ := ret[0].(error)
 	return ret0
 	return ret0
 }
 }
 
 
 // CreateStack indicates an expected call of CreateStack
 // CreateStack indicates an expected call of CreateStack
-func (mr *MockAPIMockRecorder) CreateStack(arg0, arg1, arg2 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) CreateStack(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStack", reflect.TypeOf((*MockAPI)(nil).CreateStack), arg0, arg1, arg2)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStack", reflect.TypeOf((*MockAPI)(nil).CreateStack), arg0, arg1, arg2, arg3)
 }
 }
 
 
 // DeleteAutoscalingGroup mocks base method
 // DeleteAutoscalingGroup mocks base method

+ 138 - 50
ecs/sdk.go

@@ -17,6 +17,7 @@
 package ecs
 package ecs
 
 
 import (
 import (
+	"bytes"
 	"context"
 	"context"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
@@ -30,6 +31,7 @@ import (
 
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/arn"
 	"github.com/aws/aws-sdk-go/aws/arn"
+	"github.com/aws/aws-sdk-go/aws/awserr"
 	"github.com/aws/aws-sdk-go/aws/request"
 	"github.com/aws/aws-sdk-go/aws/request"
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/service/autoscaling"
 	"github.com/aws/aws-sdk-go/service/autoscaling"
@@ -48,26 +50,32 @@ import (
 	"github.com/aws/aws-sdk-go/service/elbv2/elbv2iface"
 	"github.com/aws/aws-sdk-go/service/elbv2/elbv2iface"
 	"github.com/aws/aws-sdk-go/service/iam"
 	"github.com/aws/aws-sdk-go/service/iam"
 	"github.com/aws/aws-sdk-go/service/iam/iamiface"
 	"github.com/aws/aws-sdk-go/service/iam/iamiface"
+	"github.com/aws/aws-sdk-go/service/s3"
+	"github.com/aws/aws-sdk-go/service/s3/s3iface"
+	"github.com/aws/aws-sdk-go/service/s3/s3manager"
 	"github.com/aws/aws-sdk-go/service/secretsmanager"
 	"github.com/aws/aws-sdk-go/service/secretsmanager"
 	"github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
 	"github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
 	"github.com/aws/aws-sdk-go/service/ssm"
 	"github.com/aws/aws-sdk-go/service/ssm"
 	"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
 	"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
 	"github.com/hashicorp/go-multierror"
 	"github.com/hashicorp/go-multierror"
+	"github.com/hashicorp/go-uuid"
 	"github.com/pkg/errors"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 	"github.com/sirupsen/logrus"
 )
 )
 
 
 type sdk struct {
 type sdk struct {
-	ECS ecsiface.ECSAPI
-	EC2 ec2iface.EC2API
-	EFS efsiface.EFSAPI
-	ELB elbv2iface.ELBV2API
-	CW  cloudwatchlogsiface.CloudWatchLogsAPI
-	IAM iamiface.IAMAPI
-	CF  cloudformationiface.CloudFormationAPI
-	SM  secretsmanageriface.SecretsManagerAPI
-	SSM ssmiface.SSMAPI
-	AG  autoscalingiface.AutoScalingAPI
+	ECS      ecsiface.ECSAPI
+	EC2      ec2iface.EC2API
+	EFS      efsiface.EFSAPI
+	ELB      elbv2iface.ELBV2API
+	CW       cloudwatchlogsiface.CloudWatchLogsAPI
+	IAM      iamiface.IAMAPI
+	CF       cloudformationiface.CloudFormationAPI
+	SM       secretsmanageriface.SecretsManagerAPI
+	SSM      ssmiface.SSMAPI
+	AG       autoscalingiface.AutoScalingAPI
+	S3       s3iface.S3API
+	uploader *s3manager.Uploader
 }
 }
 
 
 // sdk implement API
 // sdk implement API
@@ -78,16 +86,18 @@ func newSDK(sess *session.Session) sdk {
 		request.AddToUserAgent(r, internal.ECSUserAgentName+"/"+internal.Version)
 		request.AddToUserAgent(r, internal.ECSUserAgentName+"/"+internal.Version)
 	})
 	})
 	return sdk{
 	return sdk{
-		ECS: ecs.New(sess),
-		EC2: ec2.New(sess),
-		EFS: efs.New(sess),
-		ELB: elbv2.New(sess),
-		CW:  cloudwatchlogs.New(sess),
-		IAM: iam.New(sess),
-		CF:  cloudformation.New(sess),
-		SM:  secretsmanager.New(sess),
-		SSM: ssm.New(sess),
-		AG:  autoscaling.New(sess),
+		ECS:      ecs.New(sess),
+		EC2:      ec2.New(sess),
+		EFS:      efs.New(sess),
+		ELB:      elbv2.New(sess),
+		CW:       cloudwatchlogs.New(sess),
+		IAM:      iam.New(sess),
+		CF:       cloudformation.New(sess),
+		SM:       secretsmanager.New(sess),
+		SSM:      ssm.New(sess),
+		AG:       autoscaling.New(sess),
+		S3:       s3.New(sess),
+		uploader: s3manager.NewUploader(sess),
 	}
 	}
 }
 }
 
 
@@ -187,11 +197,9 @@ func (s sdk) GetSubNets(ctx context.Context, vpcID string) ([]awsResource, error
 			return nil, err
 			return nil, err
 		}
 		}
 		for _, subnet := range subnets.Subnets {
 		for _, subnet := range subnets.Subnets {
-			id := aws.StringValue(subnet.SubnetId)
-			logrus.Debugf("Found SubNet %s", id)
 			ids = append(ids, existingAWSResource{
 			ids = append(ids, existingAWSResource{
 				arn: aws.StringValue(subnet.SubnetArn),
 				arn: aws.StringValue(subnet.SubnetArn),
-				id:  id,
+				id:  aws.StringValue(subnet.SubnetId),
 			})
 			})
 		}
 		}
 
 
@@ -226,39 +234,119 @@ func (s sdk) StackExists(ctx context.Context, name string) (bool, error) {
 	return len(stacks.Stacks) > 0, nil
 	return len(stacks.Stacks) > 0, nil
 }
 }
 
 
-func (s sdk) CreateStack(ctx context.Context, name string, template []byte) error {
-	logrus.Debug("Create CloudFormation stack")
+type uploadedTemplateFunc func(body *string, url *string) (string, error)
 
 
-	_, err := s.CF.CreateStackWithContext(ctx, &cloudformation.CreateStackInput{
-		OnFailure:        aws.String("DELETE"),
-		StackName:        aws.String(name),
-		TemplateBody:     aws.String(string(template)),
-		TimeoutInMinutes: nil,
-		Capabilities: []*string{
-			aws.String(cloudformation.CapabilityCapabilityIam),
-		},
-		Tags: []*cloudformation.Tag{
-			{
-				Key:   aws.String(compose.ProjectTag),
-				Value: aws.String(name),
+const cloudformationBytesLimit = 51200
+
+func (s sdk) withTemplate(ctx context.Context, name string, template []byte, region string, fn uploadedTemplateFunc) (string, error) {
+	if len(template) < cloudformationBytesLimit {
+		return fn(aws.String(string(template)), nil)
+	}
+
+	logrus.Debug("Create s3 bucket to store cloudformation template")
+	var configuration *s3.CreateBucketConfiguration
+	if region != "us-east-1" {
+		configuration = &s3.CreateBucketConfiguration{
+			LocationConstraint: aws.String(region),
+		}
+	}
+	// CloudFormation will only allow URL from a same-region bucket
+	// to avoid conflicts we suffix bucket name by region, so we can create comparable buckets in other regions.
+	bucket := "com.docker.compose." + region
+	_, err := s.S3.CreateBucket(&s3.CreateBucketInput{
+		Bucket:                    aws.String(bucket),
+		CreateBucketConfiguration: configuration,
+	})
+	if err != nil {
+		ae, ok := err.(awserr.Error)
+		if !ok {
+			return "", err
+		}
+		if ae.Code() != s3.ErrCodeBucketAlreadyOwnedByYou {
+			return "", err
+		}
+	}
+
+	key, err := uuid.GenerateUUID()
+	if err != nil {
+		return "", err
+	}
+
+	upload, err := s.uploader.UploadWithContext(ctx, &s3manager.UploadInput{
+		Key:         aws.String(key),
+		Body:        bytes.NewReader(template),
+		Bucket:      aws.String(bucket),
+		ContentType: aws.String("application/json"),
+		Tagging:     aws.String(name),
+	})
+
+	if err != nil {
+		return "", err
+	}
+
+	defer s.S3.DeleteObjects(&s3.DeleteObjectsInput{ //nolint: errcheck
+		Bucket: aws.String(bucket),
+		Delete: &s3.Delete{
+			Objects: []*s3.ObjectIdentifier{
+				{
+					Key:       aws.String(key),
+					VersionId: upload.VersionID,
+				},
 			},
 			},
 		},
 		},
 	})
 	})
+
+	return fn(nil, aws.String(upload.Location))
+}
+
+func (s sdk) CreateStack(ctx context.Context, name string, region string, template []byte) error {
+	logrus.Debug("Create CloudFormation stack")
+
+	stackID, err := s.withTemplate(ctx, name, template, region, func(body *string, url *string) (string, error) {
+		stack, err := s.CF.CreateStackWithContext(ctx, &cloudformation.CreateStackInput{
+			OnFailure:        aws.String("DELETE"),
+			StackName:        aws.String(name),
+			TemplateBody:     body,
+			TemplateURL:      url,
+			TimeoutInMinutes: nil,
+			Capabilities: []*string{
+				aws.String(cloudformation.CapabilityCapabilityIam),
+			},
+			Tags: []*cloudformation.Tag{
+				{
+					Key:   aws.String(compose.ProjectTag),
+					Value: aws.String(name),
+				},
+			},
+		})
+		if err != nil {
+			return "", err
+		}
+		return aws.StringValue(stack.StackId), nil
+	})
+	logrus.Debugf("Stack %s created", stackID)
 	return err
 	return err
 }
 }
 
 
-func (s sdk) CreateChangeSet(ctx context.Context, name string, template []byte) (string, error) {
+func (s sdk) CreateChangeSet(ctx context.Context, name string, region string, template []byte) (string, error) {
 	logrus.Debug("Create CloudFormation Changeset")
 	logrus.Debug("Create CloudFormation Changeset")
-
 	update := fmt.Sprintf("Update%s", time.Now().Format("2006-01-02-15-04-05"))
 	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(template)),
-		Capabilities: []*string{
-			aws.String(cloudformation.CapabilityCapabilityIam),
-		},
+
+	changeset, err := s.withTemplate(ctx, name, template, region, func(body *string, url *string) (string, error) {
+		changeset, err := s.CF.CreateChangeSetWithContext(ctx, &cloudformation.CreateChangeSetInput{
+			ChangeSetName: aws.String(update),
+			ChangeSetType: aws.String(cloudformation.ChangeSetTypeUpdate),
+			StackName:     aws.String(name),
+			TemplateBody:  body,
+			TemplateURL:   url,
+			Capabilities: []*string{
+				aws.String(cloudformation.CapabilityCapabilityIam),
+			},
+		})
+		if err != nil {
+			return "", err
+		}
+		return aws.StringValue(changeset.Id), err
 	})
 	})
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
@@ -267,7 +355,7 @@ func (s sdk) CreateChangeSet(ctx context.Context, name string, template []byte)
 	// we have to WaitUntilChangeSetCreateComplete even this in fail with error `ResourceNotReady`
 	// we have to WaitUntilChangeSetCreateComplete even this in fail with error `ResourceNotReady`
 	// so that we can invoke DescribeChangeSet to check status, and then we can know about the actual creation failure cause.
 	// so that we can invoke DescribeChangeSet to check status, and then we can know about the actual creation failure cause.
 	s.CF.WaitUntilChangeSetCreateCompleteWithContext(ctx, &cloudformation.DescribeChangeSetInput{ // nolint:errcheck
 	s.CF.WaitUntilChangeSetCreateCompleteWithContext(ctx, &cloudformation.DescribeChangeSetInput{ // nolint:errcheck
-		ChangeSetName: changeset.Id,
+		ChangeSetName: aws.String(changeset),
 	})
 	})
 
 
 	desc, err := s.CF.DescribeChangeSetWithContext(ctx, &cloudformation.DescribeChangeSetInput{
 	desc, err := s.CF.DescribeChangeSetWithContext(ctx, &cloudformation.DescribeChangeSetInput{
@@ -275,10 +363,10 @@ func (s sdk) CreateChangeSet(ctx context.Context, name string, template []byte)
 		StackName:     aws.String(name),
 		StackName:     aws.String(name),
 	})
 	})
 	if aws.StringValue(desc.Status) == "FAILED" {
 	if aws.StringValue(desc.Status) == "FAILED" {
-		return *changeset.Id, fmt.Errorf(aws.StringValue(desc.StatusReason))
+		return changeset, fmt.Errorf(aws.StringValue(desc.StatusReason))
 	}
 	}
 
 
-	return *changeset.Id, err
+	return changeset, err
 }
 }
 
 
 func (s sdk) UpdateStack(ctx context.Context, changeset string) error {
 func (s sdk) UpdateStack(ctx context.Context, changeset string) error {

+ 2 - 2
ecs/up.go

@@ -44,7 +44,7 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, detach b
 	operation := stackCreate
 	operation := stackCreate
 	if update {
 	if update {
 		operation = stackUpdate
 		operation = stackUpdate
-		changeset, err := b.aws.CreateChangeSet(ctx, project.Name, template)
+		changeset, err := b.aws.CreateChangeSet(ctx, project.Name, b.Region, template)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
@@ -53,7 +53,7 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, detach b
 			return err
 			return err
 		}
 		}
 	} else {
 	} else {
-		err = b.aws.CreateStack(ctx, project.Name, template)
+		err = b.aws.CreateStack(ctx, project.Name, b.Region, template)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}

+ 1 - 0
go.mod

@@ -40,6 +40,7 @@ require (
 	github.com/google/uuid v1.1.2
 	github.com/google/uuid v1.1.2
 	github.com/gorilla/mux v1.7.4 // indirect
 	github.com/gorilla/mux v1.7.4 // indirect
 	github.com/hashicorp/go-multierror v1.1.0
 	github.com/hashicorp/go-multierror v1.1.0
+	github.com/hashicorp/go-uuid v1.0.1
 	github.com/iancoleman/strcase v0.1.2
 	github.com/iancoleman/strcase v0.1.2
 	github.com/joho/godotenv v1.3.0
 	github.com/joho/godotenv v1.3.0
 	github.com/labstack/echo v3.3.10+incompatible
 	github.com/labstack/echo v3.3.10+incompatible

+ 1 - 0
go.sum

@@ -298,6 +298,7 @@ github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa
 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
 github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
 github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
 github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
 github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=