mirror of
https://github.com/docker/compose.git
synced 2025-07-27 07:34:10 +02:00
Define amazon.API as a simplified and currated interface over AWS SDK
This makes code simpler to read and easier to mock within tests Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
parent
52440a4732
commit
8c0fee5abf
@ -1,3 +1,6 @@
|
|||||||
|
clean:
|
||||||
|
rm -rf dist/
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build -v -o dist/docker-ecs cmd/main/main.go
|
go build -v -o dist/docker-ecs cmd/main/main.go
|
||||||
|
|
||||||
|
22
ecs/pkg/amazon/api.go
Normal file
22
ecs/pkg/amazon/api.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package amazon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/awslabs/goformation/v4/cloudformation"
|
||||||
|
)
|
||||||
|
|
||||||
|
type API interface {
|
||||||
|
ClusterExists(name string) (bool, error)
|
||||||
|
CreateCluster(name string) (string, error)
|
||||||
|
DeleteCluster(name string) error
|
||||||
|
|
||||||
|
GetDefaultVPC() (string, error)
|
||||||
|
GetSubNets(vpcId string) ([]string, error)
|
||||||
|
|
||||||
|
ListRolesForPolicy(policy string) ([]string, error)
|
||||||
|
GetRoleArn(name string) (string, error)
|
||||||
|
|
||||||
|
StackExists(name string) (bool, error)
|
||||||
|
CreateStack(name string, template *cloudformation.Template) error
|
||||||
|
DescribeStackEvents(stack string) error
|
||||||
|
DeleteStack(name string) error
|
||||||
|
}
|
@ -3,18 +3,6 @@ package amazon
|
|||||||
import (
|
import (
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
"github.com/aws/aws-sdk-go/service/cloudformation"
|
|
||||||
"github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
|
|
||||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
|
||||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
|
|
||||||
"github.com/aws/aws-sdk-go/service/ec2"
|
|
||||||
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
|
|
||||||
"github.com/aws/aws-sdk-go/service/ecs"
|
|
||||||
"github.com/aws/aws-sdk-go/service/ecs/ecsiface"
|
|
||||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
|
||||||
"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/iamiface"
|
|
||||||
"github.com/docker/ecs-plugin/pkg/compose"
|
"github.com/docker/ecs-plugin/pkg/compose"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,26 +23,14 @@ func NewClient(profile string, cluster string, region string) (compose.API, erro
|
|||||||
return &client{
|
return &client{
|
||||||
Cluster: cluster,
|
Cluster: cluster,
|
||||||
Region: region,
|
Region: region,
|
||||||
sess: sess,
|
api: NewAPI(sess),
|
||||||
ECS: ecs.New(sess),
|
|
||||||
EC2: ec2.New(sess),
|
|
||||||
ELB: elbv2.New(sess),
|
|
||||||
CW: cloudwatchlogs.New(sess),
|
|
||||||
IAM: iam.New(sess),
|
|
||||||
CF: cloudformation.New(sess),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type client struct {
|
type client struct {
|
||||||
Cluster string
|
Cluster string
|
||||||
Region string
|
Region string
|
||||||
sess *session.Session
|
api API
|
||||||
ECS ecsiface.ECSAPI
|
|
||||||
EC2 ec2iface.EC2API
|
|
||||||
ELB elbv2iface.ELBV2API
|
|
||||||
CW cloudwatchlogsiface.CloudWatchLogsAPI
|
|
||||||
IAM iamiface.IAMAPI
|
|
||||||
CF cloudformationiface.CloudFormationAPI
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ compose.API = &client{}
|
var _ compose.API = &client{}
|
||||||
|
@ -2,6 +2,8 @@ package amazon
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/compose-spec/compose-go/types"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
ecsapi "github.com/aws/aws-sdk-go/service/ecs"
|
ecsapi "github.com/aws/aws-sdk-go/service/ecs"
|
||||||
@ -14,12 +16,12 @@ import (
|
|||||||
|
|
||||||
func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*cloudformation.Template, error) {
|
func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*cloudformation.Template, error) {
|
||||||
template := cloudformation.NewTemplate()
|
template := cloudformation.NewTemplate()
|
||||||
vpc, err := c.GetDefaultVPC()
|
vpc, err := c.api.GetDefaultVPC()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
subnets, err := c.GetSubNets(vpc)
|
subnets, err := c.api.GetSubNets(vpc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -42,7 +44,7 @@ func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*clo
|
|||||||
GroupDescription: securityGroup,
|
GroupDescription: securityGroup,
|
||||||
GroupName: securityGroup,
|
GroupName: securityGroup,
|
||||||
SecurityGroupIngress: ingresses,
|
SecurityGroupIngress: ingresses,
|
||||||
VpcId: *vpc,
|
VpcId: vpc,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, service := range project.Services {
|
for _, service := range project.Services {
|
||||||
@ -55,7 +57,7 @@ func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*clo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
definition.TaskRoleArn = *role
|
definition.TaskRoleArn = role
|
||||||
|
|
||||||
taskDefinition := fmt.Sprintf("%sTaskDefinition", service.Name)
|
taskDefinition := fmt.Sprintf("%sTaskDefinition", service.Name)
|
||||||
template.Resources[taskDefinition] = definition
|
template.Resources[taskDefinition] = definition
|
||||||
@ -78,3 +80,35 @@ func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*clo
|
|||||||
}
|
}
|
||||||
return template, nil
|
return template, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return arn.(string), nil
|
||||||
|
}
|
||||||
|
if defaultTaskExecutionRole != "" {
|
||||||
|
return defaultTaskExecutionRole, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debug("Retrieve Task Execution Role")
|
||||||
|
entities, err := c.api.ListRolesForPolicy(ECSTaskExecutionPolicy)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(entities) == 0 {
|
||||||
|
return "", fmt.Errorf("no Role is attached to AmazonECSTaskExecutionRole Policy, please provide an explicit task execution role")
|
||||||
|
}
|
||||||
|
if len(entities) > 1 {
|
||||||
|
return "", fmt.Errorf("multiple Roles are attached to AmazonECSTaskExecutionRole Policy, please provide an explicit task execution role")
|
||||||
|
}
|
||||||
|
|
||||||
|
arn, err := c.api.GetRoleArn(entities[0])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defaultTaskExecutionRole = arn
|
||||||
|
return arn, nil
|
||||||
|
}
|
@ -2,30 +2,22 @@ package amazon
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/service/cloudformation"
|
|
||||||
cf "github.com/aws/aws-sdk-go/service/cloudformation"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *client) ComposeDown(projectName *string, keepLoadBalancer, deleteCluster bool) error {
|
func (c *client) ComposeDown(projectName *string, keepLoadBalancer, deleteCluster bool) error {
|
||||||
_, err := c.CF.DeleteStack(&cloudformation.DeleteStackInput{
|
err := c.api.DeleteStack(projectName)
|
||||||
StackName: projectName,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf("Delete stack ")
|
fmt.Printf("Delete stack ")
|
||||||
if err = c.CF.WaitUntilStackDeleteComplete(&cf.DescribeStacksInput{StackName: projectName}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Printf("... done.\n")
|
|
||||||
|
|
||||||
if !deleteCluster {
|
if !deleteCluster {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Delete cluster %s", c.Cluster)
|
fmt.Printf("Delete cluster %s", c.Cluster)
|
||||||
if err = c.DeleteCluster(); err != nil {
|
if err = c.api.DeleteCluster(c.Cluster); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf("... done. \n")
|
fmt.Printf("... done. \n")
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
package amazon
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
|
||||||
"github.com/aws/aws-sdk-go/service/ecs"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c client) CreateCluster() (*string, error) {
|
|
||||||
logrus.Debug("Create cluster ", c.Cluster)
|
|
||||||
response, err := c.ECS.CreateCluster(&ecs.CreateClusterInput{ClusterName: &c.Cluster})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return response.Cluster.Status, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c client) DeleteCluster() error {
|
|
||||||
logrus.Debug("Delete cluster ", c.Cluster)
|
|
||||||
response, err := c.ECS.DeleteCluster(&ecs.DeleteClusterInput{Cluster: &c.Cluster})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *response.Cluster.Status == "INACTIVE" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return errors.New("Failed to delete cluster, status: " + *response.Cluster.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c client) ClusterExists() (bool, error) {
|
|
||||||
logrus.Debug("Check if cluster was already created: ", c.Cluster)
|
|
||||||
clusters, err := c.ECS.DescribeClusters(&ecs.DescribeClustersInput{
|
|
||||||
Clusters: []*string{aws.String(c.Cluster)},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return len(clusters.Clusters) > 0, nil
|
|
||||||
}
|
|
@ -1,136 +0,0 @@
|
|||||||
package amazon
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/ecs-plugin/pkg/compose"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
|
||||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
|
||||||
"github.com/compose-spec/compose-go/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c client) CreateLoadBalancer(project *compose.Project, subnets []*string) (*string, error) {
|
|
||||||
logrus.Debug("Create Load Balancer")
|
|
||||||
alb, err := c.ELB.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{
|
|
||||||
IpAddressType: nil,
|
|
||||||
Name: aws.String(fmt.Sprintf("%s-LoadBalancer", project.Name)),
|
|
||||||
Subnets: subnets,
|
|
||||||
Type: aws.String(elbv2.LoadBalancerTypeEnumNetwork),
|
|
||||||
Tags: []*elbv2.Tag{
|
|
||||||
{
|
|
||||||
Key: aws.String("com.docker.compose.project"),
|
|
||||||
Value: aws.String(project.Name),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return alb.LoadBalancers[0].LoadBalancerArn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c client) DeleteLoadBalancer(project *compose.Project, keepLoadBalancer bool) error {
|
|
||||||
logrus.Debug("Delete Load Balancer")
|
|
||||||
// FIXME We can tag LoadBalancer but not search by tag ?
|
|
||||||
loadBalancer, err := c.ELB.DescribeLoadBalancers(&elbv2.DescribeLoadBalancersInput{
|
|
||||||
Names: aws.StringSlice([]string{fmt.Sprintf("%s-LoadBalancer", project.Name)}),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
arn := loadBalancer.LoadBalancers[0].LoadBalancerArn
|
|
||||||
|
|
||||||
err = c.DeleteListeners(arn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.DeleteTargetGroups(arn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !keepLoadBalancer {
|
|
||||||
_, err = c.ELB.DeleteLoadBalancer(&elbv2.DeleteLoadBalancerInput{LoadBalancerArn: arn})
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c client) CreateTargetGroup(name string, vpc *string, port types.ServicePortConfig) (*string, error) {
|
|
||||||
logrus.Debugf("Create Target Group %d/%s\n", port.Target, port.Protocol)
|
|
||||||
group, err := c.ELB.CreateTargetGroup(&elbv2.CreateTargetGroupInput{
|
|
||||||
Name: aws.String(name),
|
|
||||||
Port: aws.Int64(int64(port.Target)),
|
|
||||||
Protocol: aws.String(strings.ToUpper(port.Protocol)),
|
|
||||||
TargetType: aws.String("ip"),
|
|
||||||
VpcId: vpc,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
arn := group.TargetGroups[0].TargetGroupArn
|
|
||||||
return arn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c client) DeleteTargetGroups(loadBalancer *string) error {
|
|
||||||
groups, err := c.ELB.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{
|
|
||||||
LoadBalancerArn: loadBalancer,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, group := range groups.TargetGroups {
|
|
||||||
logrus.Debugf("Delete Target Group %s\n", *group.TargetGroupArn)
|
|
||||||
_, err := c.ELB.DeleteTargetGroup(&elbv2.DeleteTargetGroupInput{
|
|
||||||
TargetGroupArn: group.TargetGroupArn,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c client) CreateListener(port types.ServicePortConfig, arn *string, target *string) error {
|
|
||||||
logrus.Debugf("Create Listener %d\n", port.Published)
|
|
||||||
_, err := c.ELB.CreateListener(&elbv2.CreateListenerInput{
|
|
||||||
DefaultActions: []*elbv2.Action{
|
|
||||||
{
|
|
||||||
ForwardConfig: &elbv2.ForwardActionConfig{
|
|
||||||
TargetGroups: []*elbv2.TargetGroupTuple{
|
|
||||||
{
|
|
||||||
TargetGroupArn: target,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Type: aws.String(elbv2.ActionTypeEnumForward),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
LoadBalancerArn: arn,
|
|
||||||
Port: aws.Int64(int64(port.Published)),
|
|
||||||
Protocol: aws.String(strings.ToUpper(port.Protocol)),
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c client) DeleteListeners(loadBalancer *string) error {
|
|
||||||
listeners, err := c.ELB.DescribeListeners(&elbv2.DescribeListenersInput{
|
|
||||||
LoadBalancerArn: loadBalancer,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, listener := range listeners.Listeners {
|
|
||||||
logrus.Debugf("Delete Listener %s\n", *listener.ListenerArn)
|
|
||||||
_, err := c.ELB.DeleteListener(&elbv2.DeleteListenerInput{
|
|
||||||
ListenerArn: listener.ListenerArn,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
package amazon
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
|
||||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
|
||||||
"github.com/docker/ecs-plugin/pkg/compose"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetOrCreateLogGroup retrieve a pre-existing log group for project or create one
|
|
||||||
func (c client) GetOrCreateLogGroup(project *compose.Project) (*string, error) {
|
|
||||||
logrus.Debug("Create Log Group")
|
|
||||||
logGroup := fmt.Sprintf("/ecs/%s", project.Name)
|
|
||||||
_, err := c.CW.CreateLogGroup(&cloudwatchlogs.CreateLogGroupInput{
|
|
||||||
LogGroupName: aws.String(logGroup),
|
|
||||||
Tags: map[string]*string{
|
|
||||||
ProjectTag: aws.String(project.Name),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(*cloudwatchlogs.ResourceAlreadyExistsException); !ok {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &logGroup, nil
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
package amazon
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
|
||||||
"github.com/aws/aws-sdk-go/service/ec2"
|
|
||||||
"github.com/compose-spec/compose-go/types"
|
|
||||||
"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.Name)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *client) ExposePort(securityGroup *string, port types.ServicePortConfig) error {
|
|
||||||
logrus.Debugf("Authorize ingress port %d/%s\n", port.Published, port.Protocol)
|
|
||||||
_, err := c.EC2.AuthorizeSecurityGroupIngress(&ec2.AuthorizeSecurityGroupIngressInput{
|
|
||||||
GroupId: securityGroup,
|
|
||||||
IpPermissions: []*ec2.IpPermission{
|
|
||||||
{
|
|
||||||
IpProtocol: aws.String(strings.ToUpper(port.Protocol)),
|
|
||||||
IpRanges: []*ec2.IpRange{
|
|
||||||
{
|
|
||||||
CidrIp: aws.String("0.0.0.0/0"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
FromPort: aws.Int64(int64(port.Target)),
|
|
||||||
ToPort: aws.Int64(int64(port.Target)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
187
ecs/pkg/amazon/sdk.go
Normal file
187
ecs/pkg/amazon/sdk.go
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
package amazon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
|
"github.com/aws/aws-sdk-go/service/cloudformation"
|
||||||
|
"github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
|
||||||
|
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||||
|
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
|
||||||
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
|
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
|
||||||
|
"github.com/aws/aws-sdk-go/service/ecs"
|
||||||
|
"github.com/aws/aws-sdk-go/service/ecs/ecsiface"
|
||||||
|
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||||
|
"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/iamiface"
|
||||||
|
cf "github.com/awslabs/goformation/v4/cloudformation"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sdk struct {
|
||||||
|
sess *session.Session
|
||||||
|
ECS ecsiface.ECSAPI
|
||||||
|
EC2 ec2iface.EC2API
|
||||||
|
ELB elbv2iface.ELBV2API
|
||||||
|
CW cloudwatchlogsiface.CloudWatchLogsAPI
|
||||||
|
IAM iamiface.IAMAPI
|
||||||
|
CF cloudformationiface.CloudFormationAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPI(sess *session.Session) API {
|
||||||
|
return sdk{
|
||||||
|
ECS: ecs.New(sess),
|
||||||
|
EC2: ec2.New(sess),
|
||||||
|
ELB: elbv2.New(sess),
|
||||||
|
CW: cloudwatchlogs.New(sess),
|
||||||
|
IAM: iam.New(sess),
|
||||||
|
CF: cloudformation.New(sess),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sdk) ClusterExists(name string) (bool, error) {
|
||||||
|
logrus.Debug("Check if cluster was already created: ", name)
|
||||||
|
clusters, err := s.ECS.DescribeClusters(&ecs.DescribeClustersInput{
|
||||||
|
Clusters: []*string{aws.String(name)},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return len(clusters.Clusters) > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sdk) CreateCluster(name string) (string, error) {
|
||||||
|
logrus.Debug("Create cluster ", name)
|
||||||
|
response, err := s.ECS.CreateCluster(&ecs.CreateClusterInput{ClusterName: aws.String(name)})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return *response.Cluster.Status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sdk) DeleteCluster(name string) error {
|
||||||
|
logrus.Debug("Delete cluster ", name)
|
||||||
|
response, err := s.ECS.DeleteCluster(&ecs.DeleteClusterInput{Cluster: aws.String(name)})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if *response.Cluster.Status == "INACTIVE" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Failed to delete cluster, status: %s" + *response.Cluster.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sdk) GetDefaultVPC() (string, error) {
|
||||||
|
logrus.Debug("Retrieve default VPC")
|
||||||
|
vpcs, err := s.EC2.DescribeVpcs(&ec2.DescribeVpcsInput{
|
||||||
|
Filters: []*ec2.Filter{
|
||||||
|
{
|
||||||
|
Name: aws.String("isDefault"),
|
||||||
|
Values: []*string{aws.String("true")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(vpcs.Vpcs) == 0 {
|
||||||
|
return "", fmt.Errorf("account has not default VPC")
|
||||||
|
}
|
||||||
|
return *vpcs.Vpcs[0].VpcId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sdk) GetSubNets(vpc string) ([]string, error) {
|
||||||
|
logrus.Debug("Retrieve SubNets")
|
||||||
|
subnets, err := s.EC2.DescribeSubnets(&ec2.DescribeSubnetsInput{
|
||||||
|
DryRun: nil,
|
||||||
|
Filters: []*ec2.Filter{
|
||||||
|
{
|
||||||
|
Name: aws.String("vpc-id"),
|
||||||
|
Values: []*string{ aws.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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sdk) ListRolesForPolicy(policy string) ([]string, error) {
|
||||||
|
entities, err := s.IAM.ListEntitiesForPolicy(&iam.ListEntitiesForPolicyInput{
|
||||||
|
EntityFilter: aws.String("Role"),
|
||||||
|
PolicyArn: aws.String(policy),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
roles := []string{}
|
||||||
|
for _, e := range entities.PolicyRoles {
|
||||||
|
roles = append(roles, *e.RoleName)
|
||||||
|
}
|
||||||
|
return roles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sdk) GetRoleArn(name string) (string, error) {
|
||||||
|
role, err := s.IAM.GetRole(&iam.GetRoleInput{
|
||||||
|
RoleName: aws.String(name),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return *role.Role.Arn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sdk) StackExists(name string) (bool, error) {
|
||||||
|
stacks, err := s.CF.DescribeStacks(&cloudformation.DescribeStacksInput{
|
||||||
|
StackName: aws.String(name),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// FIXME doesn't work as expected
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return len(stacks.Stacks) > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sdk) CreateStack(name string, template *cf.Template) error {
|
||||||
|
logrus.Debug("Create CloudFormation stack")
|
||||||
|
json, err := template.JSON()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.CF.CreateStack(&cloudformation.CreateStackInput{
|
||||||
|
OnFailure: aws.String("DELETE"),
|
||||||
|
StackName: aws.String(name),
|
||||||
|
TemplateBody: aws.String(string(json)),
|
||||||
|
TimeoutInMinutes: aws.Int64(10),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sdk) DescribeStackEvents(name string) error {
|
||||||
|
// Fixme implement Paginator on Events and return as a chan(events)
|
||||||
|
_, err := s.CF.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{
|
||||||
|
StackName: aws.String(name),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sdk) DeleteStack(name string) error {
|
||||||
|
logrus.Debug("Delete CloudFormation stack")
|
||||||
|
_, err := s.CF.DeleteStack(&cloudformation.DeleteStackInput{
|
||||||
|
StackName: aws.String(name),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
@ -2,26 +2,19 @@ package amazon
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
|
||||||
"github.com/aws/aws-sdk-go/service/cloudformation"
|
|
||||||
"github.com/docker/ecs-plugin/pkg/compose"
|
"github.com/docker/ecs-plugin/pkg/compose"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *client) ComposeUp(project *compose.Project, loadBalancerArn *string) error {
|
func (c *client) ComposeUp(project *compose.Project, loadBalancerArn *string) error {
|
||||||
ok, err := c.ClusterExists()
|
ok, err := c.api.ClusterExists(c.Cluster)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
c.CreateCluster()
|
c.api.CreateCluster(c.Cluster)
|
||||||
}
|
}
|
||||||
_, err = c.CF.DescribeStacks(&cloudformation.DescribeStacksInput{
|
update, err := c.api.StackExists(project.Name)
|
||||||
StackName: aws.String(project.Name),
|
if update {
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
// FIXME no ErrNotFound err type here
|
|
||||||
return fmt.Errorf("we do not (yet) support updating an existing CloudFormation stack")
|
return fmt.Errorf("we do not (yet) support updating an existing CloudFormation stack")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,40 +23,12 @@ func (c *client) ComposeUp(project *compose.Project, loadBalancerArn *string) er
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
json, err := template.JSON()
|
err = c.api.CreateStack(project.Name, template)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = c.CF.ValidateTemplate(&cloudformation.ValidateTemplateInput{
|
err = c.api.DescribeStackEvents(project.Name)
|
||||||
TemplateBody: aws.String(string(json)),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = c.CF.CreateStack(&cloudformation.CreateStackInput{
|
|
||||||
OnFailure: aws.String("DELETE"),
|
|
||||||
StackName: aws.String(project.Name),
|
|
||||||
TemplateBody: aws.String(string(json)),
|
|
||||||
TimeoutInMinutes: aws.Int64(10),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
events, err := c.CF.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{
|
|
||||||
StackName: aws.String(project.Name),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, event := range events.StackEvents {
|
|
||||||
fmt.Printf("%s %s\n", *event.LogicalResourceId, *event.ResourceStatus)
|
|
||||||
if *event.ResourceStatus == "CREATE_FAILED" {
|
|
||||||
fmt.Fprintln(os.Stderr, event.ResourceStatusReason)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO monitor progress
|
// TODO monitor progress
|
||||||
return nil
|
return nil
|
||||||
|
Loading…
x
Reference in New Issue
Block a user