From 4542e05ddfe0182c648e45d56b3e15e0bc63a94d Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Tue, 14 Apr 2020 18:03:33 +0200 Subject: [PATCH] API calls to register services matching compose.yaml Signed-off-by: Nicolas De Loof --- ecs/pkg/amazon/client.go | 12 ++++++ ecs/pkg/amazon/compose.go | 72 ++++++++++++++++++++++++++++++- ecs/pkg/amazon/ecs.go | 21 +++++++++ ecs/pkg/amazon/logs.go | 26 +++++++++++ ecs/pkg/amazon/network.go | 90 +++++++++++++++++++++++++++++++++++++++ ecs/pkg/amazon/roles.go | 48 +++++++++++++++++++++ 6 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 ecs/pkg/amazon/ecs.go create mode 100644 ecs/pkg/amazon/logs.go create mode 100644 ecs/pkg/amazon/network.go create mode 100644 ecs/pkg/amazon/roles.go diff --git a/ecs/pkg/amazon/client.go b/ecs/pkg/amazon/client.go index e1f5d2e9b..8e98a55f0 100644 --- a/ecs/pkg/amazon/client.go +++ b/ecs/pkg/amazon/client.go @@ -3,6 +3,10 @@ package amazon import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/aws/aws-sdk-go/service/iam" "github.com/docker/ecs-plugin/pkg/compose" ) @@ -25,6 +29,10 @@ func NewClient(profile string, cluster string, region string) (compose.API, erro Cluster: cluster, Region: region, sess: sess, + ECS: ecs.New(sess), + EC2: ec2.New(sess), + CW: cloudwatchlogs.New(sess), + IAM: iam.New(sess), }, nil } @@ -32,6 +40,10 @@ type client struct { Cluster string Region string sess *session.Session + ECS *ecs.ECS + EC2 *ec2.EC2 + CW *cloudwatchlogs.CloudWatchLogs + IAM *iam.IAM } var _ compose.API = &client{} diff --git a/ecs/pkg/amazon/compose.go b/ecs/pkg/amazon/compose.go index 6a1073792..202143f7e 100644 --- a/ecs/pkg/amazon/compose.go +++ b/ecs/pkg/amazon/compose.go @@ -1,11 +1,79 @@ package amazon import ( - "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/compose-spec/compose-go/types" "github.com/docker/ecs-plugin/pkg/compose" ) func (c *client) ComposeUp(project *compose.Project) error { - fmt.Println("TODO Up") + vpc, err := c.GetDefaultVPC() + if err != nil { + return err + } + subnets, err := c.GetSubNets(vpc) + if err != nil { + return err + } + + securityGroup, err := c.CreateSecurityGroup(project, vpc) + if err != nil { + return err + } + + logGroup, err := c.GetOrCreateLogGroup(project.Name) + if err != nil { + return err + } + + for _, service := range project.Services { + err = c.CreateService(service, securityGroup, subnets, logGroup) + if err != nil { + return err + } + } return nil } + +func (c *client) CreateService(service types.ServiceConfig, securityGroup *string, subnets []*string, logGroup *string) error { + task, err := ConvertToTaskDefinition(service) + if err != nil { + return err + } + + role, err := c.GetEcsTaskExecutionRole(service) + if err != nil { + return err + } + + task.ExecutionRoleArn = role + + for _, def := range task.ContainerDefinitions { + def.LogConfiguration.Options["awslogs-group"] = logGroup + def.LogConfiguration.Options["awslogs-stream-prefix"] = aws.String(service.Name) + def.LogConfiguration.Options["awslogs-region"] = aws.String(c.Region) + } + + arn, err := c.RegisterTaskDefinition(task) + if err != nil { + return err + } + + _, err = c.ECS.CreateService(&ecs.CreateServiceInput{ + Cluster: aws.String(c.Cluster), + DesiredCount: aws.Int64(1), // FIXME get from deploy options + LaunchType: aws.String(ecs.LaunchTypeFargate), //FIXME use service.Isolation tro select EC2 vs Fargate + NetworkConfiguration: &ecs.NetworkConfiguration{ + AwsvpcConfiguration: &ecs.AwsVpcConfiguration{ + AssignPublicIp: aws.String(ecs.AssignPublicIpEnabled), + SecurityGroups: []*string{securityGroup}, + Subnets: subnets, + }, + }, + ServiceName: aws.String(service.Name), + SchedulingStrategy: aws.String(ecs.SchedulingStrategyReplica), + TaskDefinition: arn, + }) + return err +} diff --git a/ecs/pkg/amazon/ecs.go b/ecs/pkg/amazon/ecs.go new file mode 100644 index 000000000..c405b1356 --- /dev/null +++ b/ecs/pkg/amazon/ecs.go @@ -0,0 +1,21 @@ +package amazon + +import ( + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/compose-spec/compose-go/types" + "github.com/sirupsen/logrus" +) + +func ConvertToTaskDefinition(service types.ServiceConfig) (*ecs.RegisterTaskDefinitionInput, error) { + panic("Please implement me") +} + + +func (c client) RegisterTaskDefinition(task *ecs.RegisterTaskDefinitionInput) (*string, error) { + logrus.Debug("Register Task Definition") + def, err := c.ECS.RegisterTaskDefinition(task) + if err != nil { + return nil, err + } + return def.TaskDefinition.TaskDefinitionArn, err +} diff --git a/ecs/pkg/amazon/logs.go b/ecs/pkg/amazon/logs.go new file mode 100644 index 000000000..7553ba1b7 --- /dev/null +++ b/ecs/pkg/amazon/logs.go @@ -0,0 +1,26 @@ +package amazon + +import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/sirupsen/logrus" +) + +// GetOrCreateLogGroup retrieve a pre-existing log group for project or create one +func (c client) GetOrCreateLogGroup(project string) (*string, error) { + logrus.Debug("Create Log Group") + logGroup := fmt.Sprintf("/ecs/%s", project) + _, err := c.CW.CreateLogGroup(&cloudwatchlogs.CreateLogGroupInput{ + LogGroupName: aws.String(logGroup), + Tags: map[string]*string{ + ProjectTag: aws.String(project), + }, + }) + if err != nil { + if _, ok := err.(*cloudwatchlogs.ResourceAlreadyExistsException); !ok { + return nil, err + } + } + return &logGroup, nil +} diff --git a/ecs/pkg/amazon/network.go b/ecs/pkg/amazon/network.go new file mode 100644 index 000000000..d994cce1f --- /dev/null +++ b/ecs/pkg/amazon/network.go @@ -0,0 +1,90 @@ +package amazon + +import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/docker/ecs-plugin/pkg/compose" + "github.com/sirupsen/logrus" +) + +// GetDefaultVPC retrieve the default VPC for AWS account +func (c client) GetDefaultVPC() (*string, error) { + logrus.Debug("Retrieve default VPC") + vpcs, err := c.EC2.DescribeVpcs(&ec2.DescribeVpcsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("isDefault"), + Values: []*string{aws.String("true")}, + }, + }, + }) + if err != nil { + return nil, err + } + if len(vpcs.Vpcs) == 0 { + return nil, fmt.Errorf("account has not default VPC") + } + return vpcs.Vpcs[0].VpcId, nil +} + + +// GetSubNets retrieve default subnets for a VPC +func (c client) GetSubNets(vpc *string) ([]*string, error) { + logrus.Debug("Retrieve SubNets") + subnets, err := c.EC2.DescribeSubnets(&ec2.DescribeSubnetsInput{ + DryRun: nil, + Filters: []*ec2.Filter{ + { + Name: aws.String("vpc-id"), + Values: []*string{vpc}, + }, + { + Name: aws.String("default-for-az"), + Values: []*string{aws.String("true")}, + }, + }, + }) + if err != nil { + return nil, err + } + + ids := []*string{} + for _, subnet := range subnets.Subnets { + ids = append(ids, subnet.SubnetId) + } + return ids, nil +} + +// CreateSecurityGroup create a security group for the project +func (c client) CreateSecurityGroup(project *compose.Project, vpc *string) (*string, error) { + logrus.Debug("Create Security Group") + name := fmt.Sprintf("%s Security Group", project) + securityGroup, err := c.EC2.CreateSecurityGroup(&ec2.CreateSecurityGroupInput{ + Description: aws.String(name), + GroupName: aws.String(name), + VpcId: vpc, + }) + if err != nil { + return nil, err + } + + _, err = c.EC2.CreateTags(&ec2.CreateTagsInput{ + Resources: []*string{securityGroup.GroupId}, + Tags: []*ec2.Tag{ + { + Key: aws.String("Name"), + Value: aws.String(name), + }, + { + Key: aws.String(ProjectTag), + Value: aws.String(project.Name), + }, + }, + }) + if err != nil { + return nil, err + } + + return securityGroup.GroupId, nil +} \ No newline at end of file diff --git a/ecs/pkg/amazon/roles.go b/ecs/pkg/amazon/roles.go new file mode 100644 index 000000000..3e8c5303a --- /dev/null +++ b/ecs/pkg/amazon/roles.go @@ -0,0 +1,48 @@ +package amazon + +import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/compose-spec/compose-go/types" + "github.com/sirupsen/logrus" +) + +const ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + +var defaultTaskExecutionRole *string + +// GetEcsTaskExecutionRole retrieve the role ARN to apply for task execution +func (c client) GetEcsTaskExecutionRole(spec types.ServiceConfig) (*string, error) { + if arn, ok := spec.Extras["x-ecs-TaskExecutionRole"]; ok { + s := arn.(string) + return &s, nil + } + if defaultTaskExecutionRole != nil { + return defaultTaskExecutionRole, nil + } + + logrus.Debug("Retrieve Task Execution Role") + entities, err := c.IAM.ListEntitiesForPolicy(&iam.ListEntitiesForPolicyInput{ + EntityFilter: aws.String("Role"), + PolicyArn: aws.String(ECSTaskExecutionPolicy), + }) + if err != nil { + return nil, err + } + if len(entities.PolicyRoles) == 0 { + return nil, fmt.Errorf("no Role is attached to AmazonECSTaskExecutionRole Policy, please provide an explicit task execution role") + } + if len(entities.PolicyRoles) > 1 { + return nil, fmt.Errorf("multiple Roles are attached to AmazonECSTaskExecutionRole Policy, please provide an explicit task execution role") + } + + role, err := c.IAM.GetRole(&iam.GetRoleInput{ + RoleName: entities.PolicyRoles[0].RoleName, + }) + if err != nil { + return nil, err + } + defaultTaskExecutionRole = role.Role.Arn + return role.Role.Arn, nil +}