diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index 0b1e70e6..55eb7a14 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -3,9 +3,12 @@ package commands import ( "context" "fmt" + "io" + "os" + "strings" "github.com/docker/cli/cli/command" - "github.com/docker/ecs-plugin/pkg/amazon" + amazon "github.com/docker/ecs-plugin/pkg/amazon/backend" "github.com/docker/ecs-plugin/pkg/compose" "github.com/docker/ecs-plugin/pkg/docker" "github.com/spf13/cobra" @@ -47,11 +50,11 @@ func ConvertCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) if err != nil { return err } - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } - template, err := client.Convert(project) + template, err := backend.Convert(project) if err != nil { return err } @@ -77,11 +80,11 @@ func UpCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobr if err != nil { return err } - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } - return client.ComposeUp(context.Background(), project) + return backend.ComposeUp(context.Background(), project) }), } cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "") @@ -97,11 +100,20 @@ func PsCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobr if err != nil { return err } - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } - return client.ComposePs(context.Background(), project) + tasks, err := backend.ComposePs(context.Background(), project) + if err != nil { + return err + } + printSection(os.Stdout, len(tasks), func(w io.Writer) { + for _, task := range tasks { + fmt.Fprintf(w, "%s\t%s\t%s\n", task.Name, task.State, strings.Join(task.Ports, " ")) + } + }, "NAME", "STATE", "PORTS") + return nil }), } cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "") @@ -117,7 +129,7 @@ func DownCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *co cmd := &cobra.Command{ Use: "down", RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } @@ -126,11 +138,11 @@ func DownCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *co if err != nil { return err } - return client.ComposeDown(context.Background(), project.Name, opts.DeleteCluster) + return backend.ComposeDown(context.Background(), project.Name, opts.DeleteCluster) } // project names passed as parameters for _, name := range args { - err := client.ComposeDown(context.Background(), name, opts.DeleteCluster) + err := backend.ComposeDown(context.Background(), name, opts.DeleteCluster) if err != nil { return err } @@ -146,7 +158,7 @@ func LogsCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *co cmd := &cobra.Command{ Use: "logs [PROJECT NAME]", RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } @@ -161,7 +173,7 @@ func LogsCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *co } else { name = args[0] } - return client.ComposeLogs(context.Background(), name) + return backend.ComposeLogs(context.Background(), name) }), } return cmd diff --git a/ecs/cmd/commands/secret.go b/ecs/cmd/commands/secret.go index f964f1ea..a426740d 100644 --- a/ecs/cmd/commands/secret.go +++ b/ecs/cmd/commands/secret.go @@ -10,7 +10,8 @@ import ( "text/tabwriter" "github.com/docker/cli/cli/command" - "github.com/docker/ecs-plugin/pkg/amazon" + amazon "github.com/docker/ecs-plugin/pkg/amazon/backend" + "github.com/docker/ecs-plugin/pkg/amazon/types" "github.com/docker/ecs-plugin/pkg/docker" "github.com/spf13/cobra" ) @@ -47,7 +48,7 @@ func CreateSecret(dockerCli command.Cli) *cobra.Command { Use: "create NAME", Short: "Creates a secret.", RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } @@ -56,8 +57,8 @@ func CreateSecret(dockerCli command.Cli) *cobra.Command { } name := args[0] - secret := docker.NewSecret(name, opts.Username, opts.Password, opts.Description) - id, err := client.CreateSecret(context.Background(), secret) + secret := types.NewSecret(name, opts.Username, opts.Password, opts.Description) + id, err := backend.CreateSecret(context.Background(), secret) fmt.Println(id) return err }), @@ -73,7 +74,7 @@ func InspectSecret(dockerCli command.Cli) *cobra.Command { Use: "inspect ID", Short: "Displays secret details", RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } @@ -81,7 +82,7 @@ func InspectSecret(dockerCli command.Cli) *cobra.Command { return errors.New("Missing mandatory parameter: ID") } id := args[0] - secret, err := client.InspectSecret(context.Background(), id) + secret, err := backend.InspectSecret(context.Background(), id) if err != nil { return err } @@ -102,11 +103,11 @@ func ListSecrets(dockerCli command.Cli) *cobra.Command { Aliases: []string{"ls"}, Short: "List secrets stored for the existing account.", RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } - secrets, err := client.ListSecrets(context.Background()) + secrets, err := backend.ListSecrets(context.Background()) if err != nil { return err } @@ -125,21 +126,21 @@ func DeleteSecret(dockerCli command.Cli) *cobra.Command { Aliases: []string{"rm", "remove"}, Short: "Removes a secret.", RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } if len(args) == 0 { return errors.New("Missing mandatory parameter: [NAME]") } - return client.DeleteSecret(context.Background(), args[0], opts.recover) + return backend.DeleteSecret(context.Background(), args[0], opts.recover) }), } cmd.Flags().BoolVar(&opts.recover, "recover", false, "Enable recovery.") return cmd } -func printList(out io.Writer, secrets []docker.Secret) { +func printList(out io.Writer, secrets []types.Secret) { printSection(out, len(secrets), func(w io.Writer) { for _, secret := range secrets { fmt.Fprintf(w, "%s\t%s\t%s\n", secret.ID, secret.Name, secret.Description) diff --git a/ecs/pkg/amazon/amazon.go b/ecs/pkg/amazon/amazon.go new file mode 100644 index 00000000..f6c588d1 --- /dev/null +++ b/ecs/pkg/amazon/amazon.go @@ -0,0 +1,8 @@ +package amazon + +import ( + "github.com/docker/ecs-plugin/pkg/amazon/backend" + "github.com/docker/ecs-plugin/pkg/compose" +) + +var _ compose.API = &backend.Backend{} diff --git a/ecs/pkg/amazon/api.go b/ecs/pkg/amazon/api.go deleted file mode 100644 index 5b058584..00000000 --- a/ecs/pkg/amazon/api.go +++ /dev/null @@ -1,11 +0,0 @@ -package amazon - -//go:generate mockgen -destination=./api_mock.go -self_package "github.com/docker/ecs-plugin/pkg/amazon" -package=amazon . API - -type API interface { - downAPI - upAPI - logsAPI - secretsAPI - listAPI -} diff --git a/ecs/pkg/amazon/client.go b/ecs/pkg/amazon/backend/backend.go similarity index 66% rename from ecs/pkg/amazon/client.go rename to ecs/pkg/amazon/backend/backend.go index 839c5118..0d2f07f9 100644 --- a/ecs/pkg/amazon/client.go +++ b/ecs/pkg/amazon/backend/backend.go @@ -1,9 +1,9 @@ -package amazon +package backend import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" - "github.com/docker/ecs-plugin/pkg/compose" + "github.com/docker/ecs-plugin/pkg/amazon/sdk" ) const ( @@ -12,7 +12,7 @@ const ( ServiceTag = "com.docker.compose.service" ) -func NewClient(profile string, cluster string, region string) (compose.API, error) { +func NewBackend(profile string, cluster string, region string) (*Backend, error) { sess, err := session.NewSessionWithOptions(session.Options{ Profile: profile, Config: aws.Config{ @@ -22,17 +22,15 @@ func NewClient(profile string, cluster string, region string) (compose.API, erro if err != nil { return nil, err } - return &client{ + return &Backend{ Cluster: cluster, Region: region, - api: NewAPI(sess), + api: sdk.NewAPI(sess), }, nil } -type client struct { +type Backend struct { Cluster string Region string - api API + api sdk.API } - -var _ compose.API = &client{} diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go similarity index 85% rename from ecs/pkg/amazon/cloudformation.go rename to ecs/pkg/amazon/backend/cloudformation.go index 51eba2c4..115a67d6 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -1,4 +1,4 @@ -package amazon +package backend import ( "fmt" @@ -21,6 +21,9 @@ import ( "github.com/awslabs/goformation/v4/cloudformation/logs" cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery" "github.com/awslabs/goformation/v4/cloudformation/tags" + "github.com/docker/ecs-plugin/pkg/amazon/compatibility" + sdk "github.com/docker/ecs-plugin/pkg/amazon/sdk" + btypes "github.com/docker/ecs-plugin/pkg/amazon/types" "github.com/docker/ecs-plugin/pkg/compose" ) @@ -33,8 +36,8 @@ const ( ) // Convert a compose project into a CloudFormation template -func (c client) Convert(project *compose.Project) (*cloudformation.Template, error) { - warnings := Check(project) +func (b Backend) Convert(project *compose.Project) (*cloudformation.Template, error) { + warnings := compatibility.Check(project) for _, w := range warnings { logrus.Warn(w) } @@ -75,7 +78,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err // Create Cluster is `ParameterClusterName` parameter is not set template.Conditions["CreateCluster"] = cloudformation.Equals("", cloudformation.Ref(ParameterClusterName)) - cluster := c.createCluster(project, template) + cluster := createCluster(project, template) networks := map[string]string{} for _, net := range project.Networks { @@ -88,17 +91,18 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err } // Private DNS namespace will allow DNS name for the services to be ..local - c.createCloudMap(project, template) + createCloudMap(project, template) - loadBalancerARN := c.createLoadBalancer(project, template) + loadBalancerARN := createLoadBalancer(project, template) for _, service := range project.Services { - definition, err := Convert(project, service) + + definition, err := sdk.Convert(project, service) if err != nil { return nil, err } - taskExecutionRole, err := c.createTaskExecutionRole(service, err, definition, template) + taskExecutionRole, err := createTaskExecutionRole(service, err, definition, template) if err != nil { return template, err } @@ -112,7 +116,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err // FIXME ECS only support HTTP(s) health checks, while Docker only support CMD } - serviceRegistry := c.createServiceRegistry(service, template, healthCheck) + serviceRegistry := createServiceRegistry(service, template, healthCheck) serviceSecurityGroups := []string{} for net := range service.Networks { @@ -124,14 +128,14 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err if len(service.Ports) > 0 { for _, port := range service.Ports { protocol := strings.ToUpper(port.Protocol) - if c.getLoadBalancerType(project) == elbv2.LoadBalancerTypeEnumApplication { + if getLoadBalancerType(project) == elbv2.LoadBalancerTypeEnumApplication { protocol = elbv2.ProtocolEnumHttps if port.Published == 80 { protocol = elbv2.ProtocolEnumHttp } } - targetGroupName := c.createTargetGroup(project, service, port, template, protocol) - listenerName := c.createListener(service, port, template, targetGroupName, loadBalancerARN, protocol) + targetGroupName := createTargetGroup(project, service, port, template, protocol) + listenerName := createListener(service, port, template, targetGroupName, loadBalancerARN, protocol) dependsOn = append(dependsOn, listenerName) serviceLB = append(serviceLB, ecs.Service_LoadBalancer{ ContainerName: service.Name, @@ -184,7 +188,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err return template, nil } -func (c client) getLoadBalancerType(project *compose.Project) string { +func getLoadBalancerType(project *compose.Project) string { for _, service := range project.Services { for _, port := range service.Ports { if port.Published != 80 && port.Published != 443 { @@ -195,7 +199,7 @@ func (c client) getLoadBalancerType(project *compose.Project) string { return elbv2.LoadBalancerTypeEnumApplication } -func (c client) getLoadBalancerSecurityGroups(project *compose.Project, template *cloudformation.Template) []string { +func getLoadBalancerSecurityGroups(project *compose.Project, template *cloudformation.Template) []string { securityGroups := []string{} for _, network := range project.Networks { if !network.Internal { @@ -206,15 +210,15 @@ func (c client) getLoadBalancerSecurityGroups(project *compose.Project, template return uniqueStrings(securityGroups) } -func (c client) createLoadBalancer(project *compose.Project, template *cloudformation.Template) string { +func createLoadBalancer(project *compose.Project, template *cloudformation.Template) string { loadBalancerName := fmt.Sprintf("%sLoadBalancer", strings.Title(project.Name)) // Create LoadBalancer if `ParameterLoadBalancerName` is not set template.Conditions["CreateLoadBalancer"] = cloudformation.Equals("", cloudformation.Ref(ParameterLoadBalancerARN)) - loadBalancerType := c.getLoadBalancerType(project) + loadBalancerType := getLoadBalancerType(project) securityGroups := []string{} if loadBalancerType == elbv2.LoadBalancerTypeEnumApplication { - securityGroups = c.getLoadBalancerSecurityGroups(project, template) + securityGroups = getLoadBalancerSecurityGroups(project, template) } template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{ @@ -237,7 +241,7 @@ func (c client) createLoadBalancer(project *compose.Project, template *cloudform return cloudformation.If("CreateLoadBalancer", cloudformation.Ref(loadBalancerName), cloudformation.Ref(ParameterLoadBalancerARN)) } -func (c client) createListener(service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, targetGroupName string, loadBalancerARN string, protocol string) string { +func createListener(service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, targetGroupName string, loadBalancerARN string, protocol string) string { listenerName := fmt.Sprintf( "%s%s%dListener", normalizeResourceName(service.Name), @@ -266,7 +270,7 @@ func (c client) createListener(service types.ServiceConfig, port types.ServicePo return listenerName } -func (c client) createTargetGroup(project *compose.Project, service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, protocol string) string { +func createTargetGroup(project *compose.Project, service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, protocol string) string { targetGroupName := fmt.Sprintf( "%s%s%dTargetGroup", normalizeResourceName(service.Name), @@ -289,7 +293,7 @@ func (c client) createTargetGroup(project *compose.Project, service types.Servic return targetGroupName } -func (c client) createServiceRegistry(service types.ServiceConfig, template *cloudformation.Template, healthCheck *cloudmap.Service_HealthCheckConfig) ecs.Service_ServiceRegistry { +func createServiceRegistry(service types.ServiceConfig, template *cloudformation.Template, healthCheck *cloudmap.Service_HealthCheckConfig) ecs.Service_ServiceRegistry { serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", normalizeResourceName(service.Name)) serviceRegistry := ecs.Service_ServiceRegistry{ RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"), @@ -316,9 +320,9 @@ func (c client) createServiceRegistry(service types.ServiceConfig, template *clo return serviceRegistry } -func (c client) createTaskExecutionRole(service types.ServiceConfig, err error, definition *ecs.TaskDefinition, template *cloudformation.Template) (string, error) { +func createTaskExecutionRole(service types.ServiceConfig, err error, definition *ecs.TaskDefinition, template *cloudformation.Template) (string, error) { taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", normalizeResourceName(service.Name)) - policy, err := c.getPolicy(definition) + policy, err := getPolicy(definition) if err != nil { return taskExecutionRole, err } @@ -341,7 +345,7 @@ func (c client) createTaskExecutionRole(service types.ServiceConfig, err error, return taskExecutionRole, nil } -func (c client) createCluster(project *compose.Project, template *cloudformation.Template) string { +func createCluster(project *compose.Project, template *cloudformation.Template) string { template.Resources["Cluster"] = &ecs.Cluster{ ClusterName: project.Name, Tags: []tags.Tag{ @@ -356,7 +360,7 @@ func (c client) createCluster(project *compose.Project, template *cloudformation return cluster } -func (c client) createCloudMap(project *compose.Project, template *cloudformation.Template) { +func createCloudMap(project *compose.Project, template *cloudformation.Template) { template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{ Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name), Name: fmt.Sprintf("%s.local", project.Name), @@ -365,7 +369,7 @@ func (c client) createCloudMap(project *compose.Project, template *cloudformatio } func convertNetwork(project *compose.Project, net types.NetworkConfig, vpc string, template *cloudformation.Template) string { - if sg, ok := net.Extras[ExtensionSecurityGroup]; ok { + if sg, ok := net.Extras[btypes.ExtensionSecurityGroup]; ok { logrus.Debugf("Security Group for network %q set by user to %q", net.Name, sg) return sg.(string) } @@ -428,7 +432,7 @@ func normalizeResourceName(s string) string { return strings.Title(regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString(s, "")) } -func (c client) getPolicy(taskDef *ecs.TaskDefinition) (*PolicyDocument, error) { +func getPolicy(taskDef *ecs.TaskDefinition) (*PolicyDocument, error) { arns := []string{} for _, container := range taskDef.ContainerDefinitions { if container.RepositoryCredentials != nil { diff --git a/ecs/pkg/amazon/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go similarity index 97% rename from ecs/pkg/amazon/cloudformation_test.go rename to ecs/pkg/amazon/backend/cloudformation_test.go index 4b525d8f..12271478 100644 --- a/ecs/pkg/amazon/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -1,4 +1,4 @@ -package amazon +package backend import ( "fmt" @@ -13,6 +13,7 @@ import ( "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" "github.com/docker/ecs-plugin/pkg/compose" + "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -103,7 +104,7 @@ services: } func convertResultAsString(t *testing.T, project *compose.Project, clusterName string) string { - client, err := NewClient("", clusterName, "") + client, err := NewBackend("", clusterName, "") assert.NilError(t, err) result, err := client.Convert(project) assert.NilError(t, err) @@ -133,7 +134,7 @@ func convertYaml(t *testing.T, yaml string) *cloudformation.Template { assert.NilError(t, err) err = compose.Normalize(model) assert.NilError(t, err) - template, err := client{}.Convert(&compose.Project{ + template, err := Backend{}.Convert(&compose.Project{ Config: *model, Name: "test", }) diff --git a/ecs/pkg/amazon/backend/down.go b/ecs/pkg/amazon/backend/down.go new file mode 100644 index 00000000..8b978e79 --- /dev/null +++ b/ecs/pkg/amazon/backend/down.go @@ -0,0 +1,31 @@ +package backend + +import ( + "context" + "fmt" + + "github.com/docker/ecs-plugin/pkg/amazon/types" +) + +func (b *Backend) ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error { + err := b.api.DeleteStack(ctx, projectName) + if err != nil { + return err + } + + err = b.WaitStackCompletion(ctx, projectName, types.StackDelete) + if err != nil { + return err + } + + if !deleteCluster { + return nil + } + + fmt.Printf("Delete cluster %s", b.Cluster) + if err = b.api.DeleteCluster(ctx, b.Cluster); err != nil { + return err + } + fmt.Printf("... done. \n") + return nil +} diff --git a/ecs/pkg/amazon/down_test.go b/ecs/pkg/amazon/backend/down_test.go similarity index 73% rename from ecs/pkg/amazon/down_test.go rename to ecs/pkg/amazon/backend/down_test.go index 642faf75..d9abf70c 100644 --- a/ecs/pkg/amazon/down_test.go +++ b/ecs/pkg/amazon/backend/down_test.go @@ -1,17 +1,19 @@ -package amazon +package backend import ( "context" "testing" + "github.com/docker/ecs-plugin/pkg/amazon/sdk" + btypes "github.com/docker/ecs-plugin/pkg/amazon/types" "github.com/golang/mock/gomock" ) func TestDownDontDeleteCluster(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - m := NewMockAPI(ctrl) - c := &client{ + m := sdk.NewMockAPI(ctrl) + c := &Backend{ Cluster: "test_cluster", Region: "region", api: m, @@ -20,7 +22,7 @@ func TestDownDontDeleteCluster(t *testing.T) { recorder := m.EXPECT() recorder.DeleteStack(ctx, "test_project").Return(nil) recorder.GetStackID(ctx, "test_project").Return("stack-123", nil) - recorder.WaitStackComplete(ctx, "stack-123", StackDelete).Return(nil) + recorder.WaitStackComplete(ctx, "stack-123", btypes.StackDelete).Return(nil) recorder.DescribeStackEvents(ctx, "stack-123").Return(nil, nil) c.ComposeDown(ctx, "test_project", false) @@ -29,8 +31,8 @@ func TestDownDontDeleteCluster(t *testing.T) { func TestDownDeleteCluster(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - m := NewMockAPI(ctrl) - c := &client{ + m := sdk.NewMockAPI(ctrl) + c := &Backend{ Cluster: "test_cluster", Region: "region", api: m, @@ -40,7 +42,7 @@ func TestDownDeleteCluster(t *testing.T) { recorder := m.EXPECT() recorder.DeleteStack(ctx, "test_project").Return(nil) recorder.GetStackID(ctx, "test_project").Return("stack-123", nil) - recorder.WaitStackComplete(ctx, "stack-123", StackDelete).Return(nil) + recorder.WaitStackComplete(ctx, "stack-123", btypes.StackDelete).Return(nil) recorder.DescribeStackEvents(ctx, "stack-123").Return(nil, nil) recorder.DeleteCluster(ctx, "test_cluster").Return(nil) diff --git a/ecs/pkg/amazon/iam.go b/ecs/pkg/amazon/backend/iam.go similarity index 98% rename from ecs/pkg/amazon/iam.go rename to ecs/pkg/amazon/backend/iam.go index affcaaae..81a4fdb0 100644 --- a/ecs/pkg/amazon/iam.go +++ b/ecs/pkg/amazon/backend/iam.go @@ -1,4 +1,4 @@ -package amazon +package backend const ( ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" diff --git a/ecs/pkg/amazon/backend/list.go b/ecs/pkg/amazon/backend/list.go new file mode 100644 index 00000000..055362aa --- /dev/null +++ b/ecs/pkg/amazon/backend/list.go @@ -0,0 +1,63 @@ +package backend + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/docker/ecs-plugin/pkg/amazon/types" + "github.com/docker/ecs-plugin/pkg/compose" +) + +func (b *Backend) ComposePs(ctx context.Context, project *compose.Project) ([]types.TaskStatus, error) { + cluster := b.Cluster + if cluster == "" { + cluster = project.Name + } + arns := []string{} + for _, service := range project.Services { + tasks, err := b.api.ListTasks(ctx, cluster, service.Name) + if err != nil { + return []types.TaskStatus{}, err + } + arns = append(arns, tasks...) + } + if len(arns) == 0 { + return []types.TaskStatus{}, nil + } + + tasks, err := b.api.DescribeTasks(ctx, cluster, arns...) + if err != nil { + return []types.TaskStatus{}, err + } + + networkInterfaces := []string{} + for _, t := range tasks { + if t.NetworkInterface != "" { + networkInterfaces = append(networkInterfaces, t.NetworkInterface) + } + } + publicIps, err := b.api.GetPublicIPs(ctx, networkInterfaces...) + if err != nil { + return []types.TaskStatus{}, err + } + + sort.Slice(tasks, func(i, j int) bool { + return strings.Compare(tasks[i].Service, tasks[j].Service) < 0 + }) + + for i, t := range tasks { + ports := []string{} + s, err := project.GetService(t.Service) + if err != nil { + return []types.TaskStatus{}, err + } + for _, p := range s.Ports { + ports = append(ports, fmt.Sprintf("%s:%d->%d/%s", publicIps[t.NetworkInterface], p.Published, p.Target, p.Protocol)) + } + tasks[i].Name = s.Name + tasks[i].Ports = ports + } + return tasks, nil +} diff --git a/ecs/pkg/amazon/logs.go b/ecs/pkg/amazon/backend/logs.go similarity index 73% rename from ecs/pkg/amazon/logs.go rename to ecs/pkg/amazon/backend/logs.go index 683ecc26..17963529 100644 --- a/ecs/pkg/amazon/logs.go +++ b/ecs/pkg/amazon/backend/logs.go @@ -1,4 +1,4 @@ -package amazon +package backend import ( "context" @@ -11,8 +11,8 @@ import ( "github.com/docker/ecs-plugin/pkg/console" ) -func (c *client) ComposeLogs(ctx context.Context, projectName string) error { - err := c.api.GetLogs(ctx, projectName, &logConsumer{ +func (b *Backend) ComposeLogs(ctx context.Context, projectName string) error { + err := b.api.GetLogs(ctx, projectName, &logConsumer{ colors: map[string]console.ColorFunc{}, width: 0, }) @@ -26,11 +26,6 @@ func (c *client) ComposeLogs(ctx context.Context, projectName string) error { return nil } -type logConsumer struct { - colors map[string]console.ColorFunc - width int -} - func (l *logConsumer) Log(service, container, message string) { cf, ok := l.colors[service] if !ok { @@ -54,10 +49,7 @@ func (l *logConsumer) computeWidth() { l.width = width + 3 } -type LogConsumer interface { - Log(service, container, message string) -} - -type logsAPI interface { - GetLogs(ctx context.Context, name string, consumer LogConsumer) error +type logConsumer struct { + colors map[string]console.ColorFunc + width int } diff --git a/ecs/pkg/amazon/backend/secrets.go b/ecs/pkg/amazon/backend/secrets.go new file mode 100644 index 00000000..f2ae7c67 --- /dev/null +++ b/ecs/pkg/amazon/backend/secrets.go @@ -0,0 +1,23 @@ +package backend + +import ( + "context" + + "github.com/docker/ecs-plugin/pkg/amazon/types" +) + +func (b Backend) CreateSecret(ctx context.Context, secret types.Secret) (string, error) { + return b.api.CreateSecret(ctx, secret) +} + +func (b Backend) InspectSecret(ctx context.Context, id string) (types.Secret, error) { + return b.api.InspectSecret(ctx, id) +} + +func (b Backend) ListSecrets(ctx context.Context) ([]types.Secret, error) { + return b.api.ListSecrets(ctx) +} + +func (b Backend) DeleteSecret(ctx context.Context, id string, recover bool) error { + return b.api.DeleteSecret(ctx, id, recover) +} diff --git a/ecs/pkg/amazon/testdata/input/simple-single-service-with-overrides.yaml b/ecs/pkg/amazon/backend/testdata/input/simple-single-service-with-overrides.yaml similarity index 100% rename from ecs/pkg/amazon/testdata/input/simple-single-service-with-overrides.yaml rename to ecs/pkg/amazon/backend/testdata/input/simple-single-service-with-overrides.yaml diff --git a/ecs/pkg/amazon/testdata/input/simple-single-service.yaml b/ecs/pkg/amazon/backend/testdata/input/simple-single-service.yaml similarity index 100% rename from ecs/pkg/amazon/testdata/input/simple-single-service.yaml rename to ecs/pkg/amazon/backend/testdata/input/simple-single-service.yaml diff --git a/ecs/pkg/amazon/testdata/invalid_network_mode.yaml b/ecs/pkg/amazon/backend/testdata/invalid_network_mode.yaml similarity index 100% rename from ecs/pkg/amazon/testdata/invalid_network_mode.yaml rename to ecs/pkg/amazon/backend/testdata/invalid_network_mode.yaml diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden similarity index 100% rename from ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden rename to ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden similarity index 100% rename from ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden rename to ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden diff --git a/ecs/pkg/amazon/backend/up.go b/ecs/pkg/amazon/backend/up.go new file mode 100644 index 00000000..8a7a895f --- /dev/null +++ b/ecs/pkg/amazon/backend/up.go @@ -0,0 +1,100 @@ +package backend + +import ( + "context" + "fmt" + + "github.com/docker/ecs-plugin/pkg/amazon/types" + "github.com/docker/ecs-plugin/pkg/compose" +) + +func (b *Backend) ComposeUp(ctx context.Context, project *compose.Project) error { + if b.Cluster != "" { + ok, err := b.api.ClusterExists(ctx, b.Cluster) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("configured cluster %q does not exist", b.Cluster) + } + } + + update, err := b.api.StackExists(ctx, project.Name) + if err != nil { + return err + } + if update { + return fmt.Errorf("we do not (yet) support updating an existing CloudFormation stack") + } + + template, err := b.Convert(project) + if err != nil { + return err + } + + vpc, err := b.GetVPC(ctx, project) + if err != nil { + return err + } + + subNets, err := b.api.GetSubNets(ctx, vpc) + if err != nil { + return err + } + + lb, err := b.GetLoadBalancer(ctx, project) + if err != nil { + return err + } + + parameters := map[string]string{ + ParameterClusterName: b.Cluster, + ParameterVPCId: vpc, + ParameterSubnet1Id: subNets[0], + ParameterSubnet2Id: subNets[1], + ParameterLoadBalancerARN: lb, + } + + err = b.api.CreateStack(ctx, project.Name, template, parameters) + if err != nil { + return err + } + + fmt.Println() + return b.WaitStackCompletion(ctx, project.Name, types.StackCreate) +} + +func (b Backend) GetVPC(ctx context.Context, project *compose.Project) (string, error) { + //check compose file for custom VPC selected + if vpc, ok := project.Extras[types.ExtensionVPC]; ok { + vpcID := vpc.(string) + ok, err := b.api.VpcExists(ctx, vpcID) + if err != nil { + return "", err + } + if !ok { + return "", fmt.Errorf("VPC does not exist: %s", vpc) + } + } + defaultVPC, err := b.api.GetDefaultVPC(ctx) + if err != nil { + return "", err + } + return defaultVPC, nil +} + +func (b Backend) GetLoadBalancer(ctx context.Context, project *compose.Project) (string, error) { + //check compose file for custom VPC selected + if lb, ok := project.Extras[types.ExtensionLB]; ok { + lbName := lb.(string) + ok, err := b.api.LoadBalancerExists(ctx, lbName) + if err != nil { + return "", err + } + if !ok { + return "", fmt.Errorf("Load Balancer does not exist: %s", lb) + } + return b.api.GetLoadBalancerARN(ctx, lbName) + } + return "", nil +} diff --git a/ecs/pkg/amazon/wait.go b/ecs/pkg/amazon/backend/wait.go similarity index 67% rename from ecs/pkg/amazon/wait.go rename to ecs/pkg/amazon/backend/wait.go index 58ae93d7..77ad844a 100644 --- a/ecs/pkg/amazon/wait.go +++ b/ecs/pkg/amazon/backend/wait.go @@ -1,4 +1,4 @@ -package amazon +package backend import ( "context" @@ -8,16 +8,15 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/docker/ecs-plugin/pkg/console" ) -func (c *client) WaitStackCompletion(ctx context.Context, name string, operation int) error { +func (b *Backend) WaitStackCompletion(ctx context.Context, name string, operation int) error { w := console.NewProgressWriter() knownEvents := map[string]struct{}{} // Get the unique Stack ID so we can collect events without getting some from previous deployments with same name - stackID, err := c.api.GetStackID(ctx, name) + stackID, err := b.api.GetStackID(ctx, name) if err != nil { return err } @@ -26,7 +25,7 @@ func (c *client) WaitStackCompletion(ctx context.Context, name string, operation done := make(chan bool) go func() { - c.api.WaitStackComplete(ctx, stackID, operation) //nolint:errcheck + b.api.WaitStackComplete(ctx, stackID, operation) //nolint:errcheck ticker.Stop() done <- true }() @@ -39,7 +38,7 @@ func (c *client) WaitStackCompletion(ctx context.Context, name string, operation completed = true case <-ticker.C: } - events, err := c.api.DescribeStackEvents(ctx, stackID) + events, err := b.api.DescribeStackEvents(ctx, stackID) if err != nil { return err } @@ -65,14 +64,3 @@ func (c *client) WaitStackCompletion(ctx context.Context, name string, operation } return stackErr } - -type waitAPI interface { - GetStackID(ctx context.Context, name string) (string, error) - WaitStackComplete(ctx context.Context, name string, operation int) error - DescribeStackEvents(ctx context.Context, stackID string) ([]*cloudformation.StackEvent, error) -} - -const ( - StackCreate = iota - StackDelete -) diff --git a/ecs/pkg/amazon/check_test.go b/ecs/pkg/amazon/check_test.go deleted file mode 100644 index 3038eee8..00000000 --- a/ecs/pkg/amazon/check_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package amazon - -import ( - "testing" - - "gotest.tools/v3/assert" -) - -func TestInvalidNetworkMode(t *testing.T) { - project := load(t, "testdata/invalid_network_mode.yaml") - err := Check(project) - assert.Error(t, err[0], "'network_mode' \"bridge\" is not supported") -} diff --git a/ecs/pkg/amazon/check.go b/ecs/pkg/amazon/compatibility/check.go similarity index 98% rename from ecs/pkg/amazon/check.go rename to ecs/pkg/amazon/compatibility/check.go index 169794c7..58c4d306 100644 --- a/ecs/pkg/amazon/check.go +++ b/ecs/pkg/amazon/compatibility/check.go @@ -1,4 +1,4 @@ -package amazon +package compatibility import ( "github.com/compose-spec/compose-go/types" diff --git a/ecs/pkg/amazon/compatibility/check_test.go b/ecs/pkg/amazon/compatibility/check_test.go new file mode 100644 index 00000000..f6589853 --- /dev/null +++ b/ecs/pkg/amazon/compatibility/check_test.go @@ -0,0 +1,23 @@ +package compatibility + +import ( + "testing" + + "github.com/docker/ecs-plugin/pkg/compose" + "gotest.tools/v3/assert" +) + +func load(t *testing.T, paths ...string) *compose.Project { + options := compose.ProjectOptions{ + Name: t.Name(), + ConfigPaths: paths, + } + project, err := compose.ProjectFromOptions(&options) + assert.NilError(t, err) + return project +} +func TestInvalidNetworkMode(t *testing.T) { + project := load(t, "../backend/testdata/invalid_network_mode.yaml") + err := Check(project) + assert.Error(t, err[0], "'network_mode' \"bridge\" is not supported") +} diff --git a/ecs/pkg/amazon/compatibility.go b/ecs/pkg/amazon/compatibility/compatibility.go similarity index 99% rename from ecs/pkg/amazon/compatibility.go rename to ecs/pkg/amazon/compatibility/compatibility.go index 1a7f136a..a0fff142 100644 --- a/ecs/pkg/amazon/compatibility.go +++ b/ecs/pkg/amazon/compatibility/compatibility.go @@ -1,4 +1,4 @@ -package amazon +package compatibility import ( "fmt" diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go deleted file mode 100644 index d9bee735..00000000 --- a/ecs/pkg/amazon/down.go +++ /dev/null @@ -1,34 +0,0 @@ -package amazon - -import ( - "context" - "fmt" -) - -func (c *client) ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error { - err := c.api.DeleteStack(ctx, projectName) - if err != nil { - return err - } - - err = c.WaitStackCompletion(ctx, projectName, StackDelete) - if err != nil { - return err - } - - if !deleteCluster { - return nil - } - - fmt.Printf("Delete cluster %s", c.Cluster) - if err = c.api.DeleteCluster(ctx, c.Cluster); err != nil { - return err - } - fmt.Printf("... done. \n") - return nil -} - -type downAPI interface { - DeleteStack(ctx context.Context, name string) error - DeleteCluster(ctx context.Context, name string) error -} diff --git a/ecs/pkg/amazon/list.go b/ecs/pkg/amazon/list.go deleted file mode 100644 index 98903f40..00000000 --- a/ecs/pkg/amazon/list.go +++ /dev/null @@ -1,80 +0,0 @@ -package amazon - -import ( - "context" - "fmt" - "os" - "sort" - "strings" - "text/tabwriter" - - "github.com/docker/ecs-plugin/pkg/compose" -) - -func (c *client) ComposePs(ctx context.Context, project *compose.Project) error { - cluster := c.Cluster - if cluster == "" { - cluster = project.Name - } - w := tabwriter.NewWriter(os.Stdout, 20, 2, 3, ' ', 0) - fmt.Fprintf(w, "Name\tState\tPorts\n") - defer w.Flush() - - arns := []string{} - for _, service := range project.Services { - tasks, err := c.api.ListTasks(ctx, cluster, service.Name) - if err != nil { - return err - } - arns = append(arns, tasks...) - } - if len(arns) == 0 { - return nil - } - - tasks, err := c.api.DescribeTasks(ctx, cluster, arns...) - if err != nil { - return err - } - - networkInterfaces := []string{} - for _, t := range tasks { - if t.NetworkInterface != "" { - networkInterfaces = append(networkInterfaces, t.NetworkInterface) - } - } - publicIps, err := c.api.GetPublicIPs(ctx, networkInterfaces...) - if err != nil { - return err - } - - sort.Slice(tasks, func(i, j int) bool { - return strings.Compare(tasks[i].Service, tasks[j].Service) < 0 - }) - - for _, t := range tasks { - ports := []string{} - s, err := project.GetService(t.Service) - if err != nil { - return err - } - for _, p := range s.Ports { - ports = append(ports, fmt.Sprintf("%s:%d->%d/%s", publicIps[t.NetworkInterface], p.Published, p.Target, p.Protocol)) - } - fmt.Fprintf(w, "%s\t%s\t%s\n", s.Name, t.State, strings.Join(ports, ", ")) - } - return nil -} - -type TaskStatus struct { - State string - Service string - NetworkInterface string - PublicIP string -} - -type listAPI interface { - ListTasks(ctx context.Context, cluster string, name string) ([]string, error) - DescribeTasks(ctx context.Context, cluster string, arns ...string) ([]TaskStatus, error) - GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error) -} diff --git a/ecs/pkg/amazon/sdk/api.go b/ecs/pkg/amazon/sdk/api.go new file mode 100644 index 00000000..137a4339 --- /dev/null +++ b/ecs/pkg/amazon/sdk/api.go @@ -0,0 +1,61 @@ +package sdk + +import ( + "context" + + cf "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/awslabs/goformation/v4/cloudformation" + "github.com/docker/ecs-plugin/pkg/amazon/types" +) + +//go:generate mockgen -destination=./api_mock.go -self_package "github.com/docker/ecs-plugin/pkg/amazon" -package=amazon . API + +type API interface { + downAPI + upAPI + logsAPI + secretsAPI + listAPI +} + +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) + StackExists(ctx context.Context, name string) (bool, error) + CreateStack(ctx context.Context, name string, template *cloudformation.Template, parameters map[string]string) error + + LoadBalancerExists(ctx context.Context, name string) (bool, error) + GetLoadBalancerARN(ctx context.Context, name string) (string, error) +} + +type downAPI interface { + DeleteStack(ctx context.Context, name string) error + DeleteCluster(ctx context.Context, name string) error +} + +type logsAPI interface { + GetLogs(ctx context.Context, name string, consumer types.LogConsumer) error +} + +type secretsAPI interface { + CreateSecret(ctx context.Context, secret types.Secret) (string, error) + InspectSecret(ctx context.Context, id string) (types.Secret, error) + ListSecrets(ctx context.Context) ([]types.Secret, error) + DeleteSecret(ctx context.Context, id string, recover bool) error +} + +type listAPI interface { + ListTasks(ctx context.Context, cluster string, name string) ([]string, error) + DescribeTasks(ctx context.Context, cluster string, arns ...string) ([]types.TaskStatus, error) + GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error) +} + +type waitAPI interface { + GetStackID(ctx context.Context, name string) (string, error) + WaitStackComplete(ctx context.Context, name string, operation int) error + DescribeStackEvents(ctx context.Context, stackID string) ([]*cf.StackEvent, error) +} diff --git a/ecs/pkg/amazon/api_mock.go b/ecs/pkg/amazon/sdk/api_mock.go similarity index 96% rename from ecs/pkg/amazon/api_mock.go rename to ecs/pkg/amazon/sdk/api_mock.go index cbf2b6d1..07ce79e7 100644 --- a/ecs/pkg/amazon/api_mock.go +++ b/ecs/pkg/amazon/sdk/api_mock.go @@ -2,7 +2,7 @@ // Source: github.com/docker/ecs-plugin/pkg/amazon (interfaces: API) // Package amazon is a generated GoMock package. -package amazon +package sdk import ( context "context" @@ -10,7 +10,7 @@ import ( cloudformation "github.com/aws/aws-sdk-go/service/cloudformation" cloudformation0 "github.com/awslabs/goformation/v4/cloudformation" - docker "github.com/docker/ecs-plugin/pkg/docker" + btypes "github.com/docker/ecs-plugin/pkg/amazon/types" gomock "github.com/golang/mock/gomock" ) @@ -53,7 +53,7 @@ func (mr *MockAPIMockRecorder) ClusterExists(arg0, arg1 interface{}) *gomock.Cal } // CreateSecret mocks base method -func (m *MockAPI) CreateSecret(arg0 context.Context, arg1 docker.Secret) (string, error) { +func (m *MockAPI) CreateSecret(arg0 context.Context, arg1 btypes.Secret) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateSecret", arg0, arg1) ret0, _ := ret[0].(string) @@ -139,14 +139,14 @@ func (mr *MockAPIMockRecorder) DescribeStackEvents(arg0, arg1 interface{}) *gomo } // DescribeTasks mocks base method -func (m *MockAPI) DescribeTasks(arg0 context.Context, arg1 string, arg2 ...string) ([]TaskStatus, error) { +func (m *MockAPI) DescribeTasks(arg0 context.Context, arg1 string, arg2 ...string) ([]btypes.TaskStatus, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "DescribeTasks", varargs...) - ret0, _ := ret[0].([]TaskStatus) + ret0, _ := ret[0].([]btypes.TaskStatus) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -174,7 +174,7 @@ func (mr *MockAPIMockRecorder) GetDefaultVPC(arg0 interface{}) *gomock.Call { } // GetLogs mocks base method -func (m *MockAPI) GetLogs(arg0 context.Context, arg1 string, arg2 LogConsumer) error { +func (m *MockAPI) GetLogs(arg0 context.Context, arg1 string, arg2 btypes.LogConsumer) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetLogs", arg0, arg1, arg2) ret0, _ := ret[0].(error) @@ -238,10 +238,10 @@ func (mr *MockAPIMockRecorder) GetSubNets(arg0, arg1 interface{}) *gomock.Call { } // InspectSecret mocks base method -func (m *MockAPI) InspectSecret(arg0 context.Context, arg1 string) (docker.Secret, error) { +func (m *MockAPI) InspectSecret(arg0 context.Context, arg1 string) (btypes.Secret, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "InspectSecret", arg0, arg1) - ret0, _ := ret[0].(docker.Secret) + ret0, _ := ret[0].(btypes.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -253,10 +253,10 @@ func (mr *MockAPIMockRecorder) InspectSecret(arg0, arg1 interface{}) *gomock.Cal } // ListSecrets mocks base method -func (m *MockAPI) ListSecrets(arg0 context.Context) ([]docker.Secret, error) { +func (m *MockAPI) ListSecrets(arg0 context.Context) ([]btypes.Secret, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListSecrets", arg0) - ret0, _ := ret[0].([]docker.Secret) + ret0, _ := ret[0].([]btypes.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/ecs/pkg/amazon/convert.go b/ecs/pkg/amazon/sdk/convert.go similarity index 98% rename from ecs/pkg/amazon/convert.go rename to ecs/pkg/amazon/sdk/convert.go index 82047ddb..8874aa83 100644 --- a/ecs/pkg/amazon/convert.go +++ b/ecs/pkg/amazon/sdk/convert.go @@ -1,4 +1,4 @@ -package amazon +package sdk import ( "fmt" @@ -13,6 +13,7 @@ import ( "github.com/awslabs/goformation/v4/cloudformation/tags" "github.com/compose-spec/compose-go/types" "github.com/docker/cli/opts" + t "github.com/docker/ecs-plugin/pkg/amazon/types" "github.com/docker/ecs-plugin/pkg/compose" ) @@ -318,7 +319,7 @@ func getImage(image string) string { func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_RepositoryCredentials { // extract registry and namespace string from image name for key, value := range service.Extras { - if key == ExtensionPullCredentials { + if key == t.ExtensionPullCredentials { return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: value.(string)} } } diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk/sdk.go similarity index 94% rename from ecs/pkg/amazon/sdk.go rename to ecs/pkg/amazon/sdk/sdk.go index e074fb11..1e1cb849 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -1,4 +1,4 @@ -package amazon +package sdk import ( "context" @@ -25,7 +25,8 @@ import ( cf "github.com/awslabs/goformation/v4/cloudformation" "github.com/sirupsen/logrus" - "github.com/docker/ecs-plugin/pkg/docker" + "github.com/docker/ecs-plugin/pkg/amazon/types" + t "github.com/docker/ecs-plugin/pkg/amazon/types" ) type sdk struct { @@ -188,9 +189,9 @@ func (s sdk) WaitStackComplete(ctx context.Context, name string, operation int) StackName: aws.String(name), } switch operation { - case StackCreate: + case t.StackCreate: return s.CF.WaitUntilStackCreateCompleteWithContext(ctx, input) - case StackDelete: + case t.StackDelete: return s.CF.WaitUntilStackDeleteCompleteWithContext(ctx, input) default: return fmt.Errorf("internal error: unexpected stack operation %d", operation) @@ -235,7 +236,7 @@ func (s sdk) DeleteStack(ctx context.Context, name string) error { return err } -func (s sdk) CreateSecret(ctx context.Context, secret docker.Secret) (string, error) { +func (s sdk) CreateSecret(ctx context.Context, secret t.Secret) (string, error) { logrus.Debug("Create secret " + secret.Name) secretStr, err := secret.GetCredString() if err != nil { @@ -253,17 +254,17 @@ func (s sdk) CreateSecret(ctx context.Context, secret docker.Secret) (string, er return *response.ARN, nil } -func (s sdk) InspectSecret(ctx context.Context, id string) (docker.Secret, error) { +func (s sdk) InspectSecret(ctx context.Context, id string) (t.Secret, error) { logrus.Debug("Inspect secret " + id) response, err := s.SM.DescribeSecret(&secretsmanager.DescribeSecretInput{SecretId: &id}) if err != nil { - return docker.Secret{}, err + return t.Secret{}, err } labels := map[string]string{} for _, tag := range response.Tags { labels[*tag.Key] = *tag.Value } - secret := docker.Secret{ + secret := t.Secret{ ID: *response.ARN, Name: *response.Name, Labels: labels, @@ -274,14 +275,14 @@ func (s sdk) InspectSecret(ctx context.Context, id string) (docker.Secret, error return secret, nil } -func (s sdk) ListSecrets(ctx context.Context) ([]docker.Secret, error) { +func (s sdk) ListSecrets(ctx context.Context) ([]t.Secret, error) { logrus.Debug("List secrets ...") response, err := s.SM.ListSecrets(&secretsmanager.ListSecretsInput{}) if err != nil { - return []docker.Secret{}, err + return []t.Secret{}, err } - var secrets []docker.Secret + var secrets []t.Secret for _, sec := range response.SecretList { @@ -293,7 +294,7 @@ func (s sdk) ListSecrets(ctx context.Context) ([]docker.Secret, error) { if sec.Description != nil { description = *sec.Description } - secrets = append(secrets, docker.Secret{ + secrets = append(secrets, t.Secret{ ID: *sec.ARN, Name: *sec.Name, Labels: labels, @@ -310,7 +311,7 @@ func (s sdk) DeleteSecret(ctx context.Context, id string, recover bool) error { return err } -func (s sdk) GetLogs(ctx context.Context, name string, consumer LogConsumer) error { +func (s sdk) GetLogs(ctx context.Context, name string, consumer types.LogConsumer) error { logGroup := fmt.Sprintf("/docker-compose/%s", name) var startTime int64 for { @@ -356,7 +357,7 @@ func (s sdk) ListTasks(ctx context.Context, cluster string, service string) ([]s return arns, nil } -func (s sdk) DescribeTasks(ctx context.Context, cluster string, arns ...string) ([]TaskStatus, error) { +func (s sdk) DescribeTasks(ctx context.Context, cluster string, arns ...string) ([]t.TaskStatus, error) { tasks, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{ Cluster: aws.String(cluster), Tasks: aws.StringSlice(arns), @@ -364,7 +365,7 @@ func (s sdk) DescribeTasks(ctx context.Context, cluster string, arns ...string) if err != nil { return nil, err } - result := []TaskStatus{} + result := []t.TaskStatus{} for _, task := range tasks.Tasks { var networkInterface string for _, attachement := range task.Attachments { @@ -376,7 +377,7 @@ func (s sdk) DescribeTasks(ctx context.Context, cluster string, arns ...string) } } } - result = append(result, TaskStatus{ + result = append(result, t.TaskStatus{ State: *task.LastStatus, Service: strings.Replace(*task.Group, "service:", "", 1), NetworkInterface: networkInterface, diff --git a/ecs/pkg/amazon/secrets.go b/ecs/pkg/amazon/secrets.go deleted file mode 100644 index 96a2a476..00000000 --- a/ecs/pkg/amazon/secrets.go +++ /dev/null @@ -1,30 +0,0 @@ -package amazon - -import ( - "context" - - "github.com/docker/ecs-plugin/pkg/docker" -) - -type secretsAPI interface { - CreateSecret(ctx context.Context, secret docker.Secret) (string, error) - InspectSecret(ctx context.Context, id string) (docker.Secret, error) - ListSecrets(ctx context.Context) ([]docker.Secret, error) - DeleteSecret(ctx context.Context, id string, recover bool) error -} - -func (c client) CreateSecret(ctx context.Context, secret docker.Secret) (string, error) { - return c.api.CreateSecret(ctx, secret) -} - -func (c client) InspectSecret(ctx context.Context, id string) (docker.Secret, error) { - return c.api.InspectSecret(ctx, id) -} - -func (c client) ListSecrets(ctx context.Context) ([]docker.Secret, error) { - return c.api.ListSecrets(ctx) -} - -func (c client) DeleteSecret(ctx context.Context, id string, recover bool) error { - return c.api.DeleteSecret(ctx, id, recover) -} diff --git a/ecs/pkg/docker/secret.go b/ecs/pkg/amazon/types/types.go similarity index 71% rename from ecs/pkg/docker/secret.go rename to ecs/pkg/amazon/types/types.go index 613c6263..f6815955 100644 --- a/ecs/pkg/docker/secret.go +++ b/ecs/pkg/amazon/types/types.go @@ -1,9 +1,25 @@ -package docker +package types -import ( - "encoding/json" +import "encoding/json" + +type TaskStatus struct { + Name string + State string + Service string + NetworkInterface string + PublicIP string + Ports []string +} + +const ( + StackCreate = iota + StackDelete ) +type LogConsumer interface { + Log(service, container, message string) +} + type Secret struct { ID string `json:"ID"` Name string `json:"Name"` diff --git a/ecs/pkg/amazon/x.go b/ecs/pkg/amazon/types/x.go similarity index 93% rename from ecs/pkg/amazon/x.go rename to ecs/pkg/amazon/types/x.go index 315c50c4..d8b8e011 100644 --- a/ecs/pkg/amazon/x.go +++ b/ecs/pkg/amazon/types/x.go @@ -1,4 +1,4 @@ -package amazon +package types const ( ExtensionSecurityGroup = "x-aws-securitygroup" diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go deleted file mode 100644 index 3c947724..00000000 --- a/ecs/pkg/amazon/up.go +++ /dev/null @@ -1,114 +0,0 @@ -package amazon - -import ( - "context" - "fmt" - - "github.com/awslabs/goformation/v4/cloudformation" - "github.com/docker/ecs-plugin/pkg/compose" -) - -func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error { - if c.Cluster != "" { - ok, err := c.api.ClusterExists(ctx, c.Cluster) - if err != nil { - return err - } - if !ok { - return fmt.Errorf("configured cluster %q does not exist", c.Cluster) - } - } - - update, err := c.api.StackExists(ctx, project.Name) - if err != nil { - return err - } - if update { - return fmt.Errorf("we do not (yet) support updating an existing CloudFormation stack") - } - - template, err := c.Convert(project) - if err != nil { - return err - } - - vpc, err := c.GetVPC(ctx, project) - if err != nil { - return err - } - - subNets, err := c.api.GetSubNets(ctx, vpc) - if err != nil { - return err - } - - lb, err := c.GetLoadBalancer(ctx, project) - if err != nil { - return err - } - - parameters := map[string]string{ - ParameterClusterName: c.Cluster, - ParameterVPCId: vpc, - ParameterSubnet1Id: subNets[0], - ParameterSubnet2Id: subNets[1], - ParameterLoadBalancerARN: lb, - } - - err = c.api.CreateStack(ctx, project.Name, template, parameters) - if err != nil { - return err - } - - fmt.Println() - return c.WaitStackCompletion(ctx, project.Name, StackCreate) -} - -func (c client) GetVPC(ctx context.Context, project *compose.Project) (string, error) { - //check compose file for custom VPC selected - if vpc, ok := project.Extras[ExtensionVPC]; ok { - vpcID := vpc.(string) - ok, err := c.api.VpcExists(ctx, vpcID) - if err != nil { - return "", err - } - if !ok { - return "", fmt.Errorf("VPC does not exist: %s", vpc) - } - } - defaultVPC, err := c.api.GetDefaultVPC(ctx) - if err != nil { - return "", err - } - return defaultVPC, nil -} - -func (c client) GetLoadBalancer(ctx context.Context, project *compose.Project) (string, error) { - //check compose file for custom VPC selected - if lb, ok := project.Extras[ExtensionLB]; ok { - lbName := lb.(string) - ok, err := c.api.LoadBalancerExists(ctx, lbName) - if err != nil { - return "", err - } - if !ok { - return "", fmt.Errorf("Load Balancer does not exist: %s", lb) - } - return c.api.GetLoadBalancerARN(ctx, lbName) - } - return "", 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) - StackExists(ctx context.Context, name string) (bool, error) - CreateStack(ctx context.Context, name string, template *cloudformation.Template, parameters map[string]string) error - - LoadBalancerExists(ctx context.Context, name string) (bool, error) - GetLoadBalancerARN(ctx context.Context, name string) (string, error) -} diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index 1049a993..016d4947 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -4,7 +4,7 @@ import ( "context" "github.com/awslabs/goformation/v4/cloudformation" - "github.com/docker/ecs-plugin/pkg/docker" + "github.com/docker/ecs-plugin/pkg/amazon/types" ) type API interface { @@ -13,9 +13,9 @@ type API interface { ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error ComposeLogs(ctx context.Context, projectName string) error - CreateSecret(ctx context.Context, secret docker.Secret) (string, error) - InspectSecret(ctx context.Context, id string) (docker.Secret, error) - ListSecrets(ctx context.Context) ([]docker.Secret, error) + CreateSecret(ctx context.Context, secret types.Secret) (string, error) + InspectSecret(ctx context.Context, id string) (types.Secret, error) + ListSecrets(ctx context.Context) ([]types.Secret, error) DeleteSecret(ctx context.Context, id string, recover bool) error - ComposePs(background context.Context, project *Project) error + ComposePs(background context.Context, project *Project) ([]types.TaskStatus, error) }