Create CloudFormation template with parameters

so we don't need AWS API to resolve IDs and can run conversion offline

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2020-05-12 15:22:17 +02:00
parent 69a7ef0763
commit 1fdac494f3
No known key found for this signature in database
GPG Key ID: 9858809D6F8F6E7E
9 changed files with 99 additions and 74 deletions

View File

@ -49,7 +49,7 @@ func ConvertCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions)
if err != nil {
return err
}
template, err := client.Convert(context.Background(), project)
template, err := client.Convert(project)
if err != nil {
return err
}

View File

@ -305,6 +305,7 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0=

View File

@ -5,6 +5,5 @@ package amazon
type API interface {
downAPI
upAPI
convertAPI
secretsAPI
}

View File

@ -1,8 +1,6 @@
package amazon
import (
"context"
"errors"
"fmt"
"strings"
@ -19,20 +17,31 @@ import (
"github.com/docker/ecs-plugin/pkg/compose"
)
func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudformation.Template, error) {
func (c client) Convert(project *compose.Project) (*cloudformation.Template, error) {
template := cloudformation.NewTemplate()
vpc, err := c.GetVPC(ctx, project)
if err != nil {
return nil, err
template.Parameters["VPCId"] = cloudformation.Parameter{
Type: "AWS::EC2::VPC::Id",
Description: "ID of the VPC",
}
subnets, err := c.api.GetSubNets(ctx, vpc)
if err != nil {
return nil, err
/*
FIXME can't set subnets: Ref("SubnetIds") see https://github.com/awslabs/goformation/issues/282
template.Parameters["SubnetIds"] = cloudformation.Parameter{
Type: "List<AWS::EC2::Subnet::Id>",
Description: "The list of SubnetIds, for at least two Availability Zones in the region in your VPC",
}
*/
template.Parameters["Subnet1Id"] = cloudformation.Parameter{
Type: "AWS::EC2::Subnet::Id",
Description: "SubnetId,for Availability Zone 1 in the region in your VPC",
}
template.Parameters["Subnet2Id"] = cloudformation.Parameter{
Type: "AWS::EC2::Subnet::Id",
Description: "SubnetId,for Availability Zone 1 in the region in your VPC",
}
for net := range project.Networks {
name, resource := convertNetwork(project, net, vpc)
name, resource := convertNetwork(project, net, cloudformation.Ref("VPCId"))
template.Resources[name] = resource
}
@ -45,7 +54,7 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo
template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{
Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name),
Name: fmt.Sprintf("%s.local", project.Name),
Vpc: vpc,
Vpc: cloudformation.Ref("VPCId"),
}
for _, service := range project.Services {
@ -55,7 +64,7 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo
}
taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", service.Name)
policy, err := c.getPolicy(ctx, definition)
policy, err := c.getPolicy(definition)
if err != nil {
return nil, err
}
@ -115,7 +124,10 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo
AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{
AssignPublicIp: ecsapi.AssignPublicIpEnabled,
SecurityGroups: serviceSecurityGroups,
Subnets: subnets,
Subnets: []string{
cloudformation.Ref("Subnet1Id"),
cloudformation.Ref("Subnet2Id"),
},
},
},
SchedulingStrategy: ecsapi.SchedulingStrategyReplica,
@ -171,29 +183,7 @@ func networkResourceName(project *compose.Project, network string) string {
return fmt.Sprintf("%s%sNetwork", project.Name, strings.Title(network))
}
func (c client) GetVPC(ctx context.Context, project *compose.Project) (string, error) {
//check compose file for the default external network
if net, ok := project.Networks["default"]; ok {
if net.External.External {
vpc := net.Name
ok, err := c.api.VpcExists(ctx, vpc)
if err != nil {
return "", err
}
if !ok {
return "", errors.New("Vpc does not exist: " + vpc)
}
return vpc, nil
}
}
defaultVPC, err := c.api.GetDefaultVPC(ctx)
if err != nil {
return "", err
}
return defaultVPC, nil
}
func (c client) getPolicy(ctx context.Context, taskDef *ecs.TaskDefinition) (*PolicyDocument, error) {
func (c client) getPolicy(taskDef *ecs.TaskDefinition) (*PolicyDocument, error) {
arns := []string{}
for _, container := range taskDef.ContainerDefinitions {
@ -212,17 +202,10 @@ func (c client) getPolicy(ctx context.Context, taskDef *ecs.TaskDefinition) (*Po
Statement: []PolicyStatement{
{
Effect: "Allow",
Action: []string{"secretsmanager:GetSecretValue", "ssm:GetParameters", "kms:Decrypt"},
Action: []string{ActionGetSecretValue, ActionGetParameters, ActionDecrypt},
Resource: arns,
}},
}, nil
}
return nil, nil
}
type convertAPI interface {
GetDefaultVPC(ctx context.Context) (string, error)
VpcExists(ctx context.Context, vpcID string) (bool, error)
GetSubNets(ctx context.Context, vpcID string) ([]string, error)
GetRoleArn(ctx context.Context, name string) (string, error)
}

View File

@ -1,6 +1,12 @@
package amazon
const ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
const (
ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
ActionGetSecretValue = "secretsmanager:GetSecretValue"
ActionGetParameters = "ssm:GetParameters"
ActionDecrypt = "kms:Decrypt"
)
var assumeRolePolicyDocument = PolicyDocument{
Version: "2012-10-17", // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html

View File

@ -6,12 +6,11 @@ package mock
import (
context "context"
reflect "reflect"
cloudformation "github.com/aws/aws-sdk-go/service/cloudformation"
cloudformation0 "github.com/awslabs/goformation/v4/cloudformation"
docker "github.com/docker/ecs-plugin/pkg/docker"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockAPI is a mock of API interface
@ -77,23 +76,23 @@ func (m *MockAPI) CreateSecret(arg0 context.Context, arg1 docker.Secret) (string
}
// CreateSecret indicates an expected call of CreateSecret
func (mr *MockAPIMockRecorder) CreateSecret(arg0, arg1 docker.Secret) *gomock.Call {
func (mr *MockAPIMockRecorder) CreateSecret(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSecret", reflect.TypeOf((*MockAPI)(nil).CreateSecret), arg0, arg1)
}
// CreateStack mocks base method
func (m *MockAPI) CreateStack(arg0 context.Context, arg1 string, arg2 *cloudformation0.Template) error {
func (m *MockAPI) CreateStack(arg0 context.Context, arg1 string, arg2 *cloudformation0.Template, arg3 map[string]string) error {
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)
return ret0
}
// 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()
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)
}
// DeleteCluster mocks base method
@ -168,21 +167,6 @@ func (mr *MockAPIMockRecorder) GetDefaultVPC(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultVPC", reflect.TypeOf((*MockAPI)(nil).GetDefaultVPC), arg0)
}
// GetRoleArn mocks base method
func (m *MockAPI) GetRoleArn(arg0 context.Context, arg1 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetRoleArn", arg0, arg1)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetRoleArn indicates an expected call of GetRoleArn
func (mr *MockAPIMockRecorder) GetRoleArn(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRoleArn", reflect.TypeOf((*MockAPI)(nil).GetRoleArn), arg0, arg1)
}
// GetStackID mocks base method
func (m *MockAPI) GetStackID(arg0 context.Context, arg1 string) (string, error) {
m.ctrl.T.Helper()

View File

@ -153,17 +153,27 @@ func (s sdk) StackExists(ctx context.Context, name string) (bool, error) {
return len(stacks.Stacks) > 0, nil
}
func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template) error {
func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template, parameters map[string]string) error {
logrus.Debug("Create CloudFormation stack")
json, err := template.JSON()
if err != nil {
return err
}
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),
})
}
_, err = s.CF.CreateStackWithContext(ctx, &cloudformation.CreateStackInput{
OnFailure: aws.String("DELETE"),
StackName: aws.String(name),
TemplateBody: aws.String(string(json)),
Parameters: param,
TimeoutInMinutes: aws.Int64(10),
Capabilities: []*string{
aws.String(cloudformation.CapabilityCapabilityIam),

View File

@ -29,12 +29,28 @@ func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error
return err
}
template, err := c.Convert(ctx, project)
template, err := c.Convert(project)
if err != nil {
return err
}
err = c.api.CreateStack(ctx, project.Name, template)
vpc, err := c.GetVPC(ctx, project)
if err != nil {
return err
}
subNets, err := c.api.GetSubNets(ctx, vpc)
if err != nil {
return err
}
parameters := map[string]string{
"VPCId": vpc,
"Subnet1Id": subNets[0],
"Subnet2Id": subNets[1],
}
err = c.api.CreateStack(ctx, project.Name, template, parameters)
if err != nil {
return err
}
@ -42,10 +58,36 @@ func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error
return c.WaitStackCompletion(ctx, project.Name, StackCreate)
}
func (c client) GetVPC(ctx context.Context, project *compose.Project) (string, error) {
//check compose file for the default external network
if net, ok := project.Networks["default"]; ok {
if net.External.External {
vpc := net.Name
ok, err := c.api.VpcExists(ctx, vpc)
if err != nil {
return "", err
}
if !ok {
return "", fmt.Errorf("VPC does not exist: %s", vpc)
}
return vpc, nil
}
}
defaultVPC, err := c.api.GetDefaultVPC(ctx)
if err != nil {
return "", err
}
return defaultVPC, nil
}
type upAPI interface {
waitAPI
GetDefaultVPC(ctx context.Context) (string, error)
VpcExists(ctx context.Context, vpcID string) (bool, error)
GetSubNets(ctx context.Context, vpcID string) ([]string, error)
ClusterExists(ctx context.Context, name string) (bool, error)
CreateCluster(ctx context.Context, name string) (string, error)
StackExists(ctx context.Context, name string) (bool, error)
CreateStack(ctx context.Context, name string, template *cloudformation.Template) error
CreateStack(ctx context.Context, name string, template *cloudformation.Template, parameters map[string]string) error
}

View File

@ -8,7 +8,7 @@ import (
)
type API interface {
Convert(ctx context.Context, project *Project) (*cloudformation.Template, error)
Convert(project *Project) (*cloudformation.Template, error)
ComposeUp(ctx context.Context, project *Project) error
ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error