diff --git a/aci/backend.go b/aci/backend.go index 19acedc48..7f8c39cef 100644 --- a/aci/backend.go +++ b/aci/backend.go @@ -447,6 +447,9 @@ func (cs *aciComposeService) Ps(ctx context.Context, project string) ([]compose. } return res, nil } +func (cs *aciComposeService) List(ctx context.Context, project string) ([]compose.Stack, error) { + return nil, errdefs.ErrNotImplemented +} func (cs *aciComposeService) Logs(ctx context.Context, project string, w io.Writer) error { return errdefs.ErrNotImplemented diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go index eee3bc16b..90a185560 100644 --- a/cli/cmd/compose/compose.go +++ b/cli/cmd/compose/compose.go @@ -73,6 +73,7 @@ func Command() *cobra.Command { upCommand(), downCommand(), psCommand(), + listCommand(), logsCommand(), convertCommand(), ) diff --git a/cli/cmd/compose/list.go b/cli/cmd/compose/list.go new file mode 100644 index 000000000..ba71db247 --- /dev/null +++ b/cli/cmd/compose/list.go @@ -0,0 +1,58 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package compose + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + + "github.com/docker/compose-cli/client" +) + +func listCommand() *cobra.Command { + opts := composeOptions{} + lsCmd := &cobra.Command{ + Use: "ls", + RunE: func(cmd *cobra.Command, args []string) error { + return runList(cmd.Context(), opts) + }, + } + lsCmd.Flags().StringVarP(&opts.Name, "project-name", "p", "", "Project name") + return lsCmd +} + +func runList(ctx context.Context, opts composeOptions) error { + c, err := client.New(ctx) + if err != nil { + return err + } + stackList, err := c.ComposeService().List(ctx, opts.Name) + if err != nil { + return err + } + + err = printSection(os.Stdout, func(w io.Writer) { + for _, stack := range stackList { + fmt.Fprintf(w, "%s\t%s\n", stack.Name, stack.Status) + } + }, "NAME", "STATUS") + return err +} diff --git a/client/compose.go b/client/compose.go index bd554e230..3bda30487 100644 --- a/client/compose.go +++ b/client/compose.go @@ -49,6 +49,11 @@ func (c *composeService) Ps(context.Context, string) ([]compose.ServiceStatus, e return nil, errdefs.ErrNotImplemented } +// List executes the equivalent to a `docker stack ls` +func (c *composeService) List(context.Context, string) ([]compose.Stack, error) { + return nil, errdefs.ErrNotImplemented +} + // Convert translate compose model into backend's native format func (c *composeService) Convert(context.Context, *types.Project) ([]byte, error) { return nil, errdefs.ErrNotImplemented diff --git a/compose/api.go b/compose/api.go index bb0b13c62..666290e71 100644 --- a/compose/api.go +++ b/compose/api.go @@ -33,6 +33,8 @@ type Service interface { Logs(ctx context.Context, projectName string, w io.Writer) error // Ps executes the equivalent to a `compose ps` Ps(ctx context.Context, projectName string) ([]ServiceStatus, error) + // List executes the equivalent to a `docker stack ls` + List(ctx context.Context, projectName string) ([]Stack, error) // Convert translate compose model into backend's native format Convert(ctx context.Context, project *types.Project) ([]byte, error) } @@ -54,3 +56,24 @@ type ServiceStatus struct { Ports []string Publishers []PortPublisher } + +// State of a compose stack +type State string + +const ( + // STARTING indicates that stack is being deployed + STARTING State = "starting" + // RUNNING indicates that stack is deployed and services are running + RUNNING State = "running" + // UPDATING indicates that some stack resources are being recreated + UPDATING State = "updating" + // REMOVING indicates that stack is being deleted + REMOVING State = "removing" +) + +// Stack holds the name and state of a compose application/stack +type Stack struct { + ID string + Name string + Status State +} diff --git a/ecs/list.go b/ecs/list.go index e1c643e04..cfe01ccc8 100644 --- a/ecs/list.go +++ b/ecs/list.go @@ -18,53 +18,11 @@ package ecs import ( "context" - "fmt" - "strings" "github.com/docker/compose-cli/compose" ) -func (b *ecsAPIService) Ps(ctx context.Context, project string) ([]compose.ServiceStatus, error) { - parameters, err := b.SDK.ListStackParameters(ctx, project) - if err != nil { - return nil, err - } - cluster := parameters[parameterClusterName] +func (b *ecsAPIService) List(ctx context.Context, project string) ([]compose.Stack, error) { + return b.SDK.ListStacks(ctx, project) - resources, err := b.SDK.ListStackResources(ctx, project) - if err != nil { - return nil, err - } - - servicesARN := []string{} - for _, r := range resources { - switch r.Type { - case "AWS::ECS::Service": - servicesARN = append(servicesARN, r.ARN) - case "AWS::ECS::Cluster": - cluster = r.ARN - } - } - if len(servicesARN) == 0 { - return nil, nil - } - status, err := b.SDK.DescribeServices(ctx, cluster, servicesARN) - if err != nil { - return nil, err - } - - for i, state := range status { - ports := []string{} - for _, lb := range state.Publishers { - ports = append(ports, fmt.Sprintf( - "%s:%d->%d/%s", - lb.URL, - lb.PublishedPort, - lb.TargetPort, - strings.ToLower(lb.Protocol))) - } - state.Ports = ports - status[i] = state - } - return status, nil } diff --git a/ecs/local/compose.go b/ecs/local/compose.go index ff4aa5c72..080318dec 100644 --- a/ecs/local/compose.go +++ b/ecs/local/compose.go @@ -177,3 +177,6 @@ func (e ecsLocalSimulation) Logs(ctx context.Context, projectName string, w io.W func (e ecsLocalSimulation) Ps(ctx context.Context, projectName string) ([]compose.ServiceStatus, error) { return nil, errors.Wrap(errdefs.ErrNotImplemented, "use docker-compose ps") } +func (e ecsLocalSimulation) List(ctx context.Context, projectName string) ([]compose.Stack, error) { + return nil, errors.Wrap(errdefs.ErrNotImplemented, "use docker-compose ls") +} diff --git a/ecs/ps.go b/ecs/ps.go new file mode 100644 index 000000000..e1c643e04 --- /dev/null +++ b/ecs/ps.go @@ -0,0 +1,70 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ecs + +import ( + "context" + "fmt" + "strings" + + "github.com/docker/compose-cli/compose" +) + +func (b *ecsAPIService) Ps(ctx context.Context, project string) ([]compose.ServiceStatus, error) { + parameters, err := b.SDK.ListStackParameters(ctx, project) + if err != nil { + return nil, err + } + cluster := parameters[parameterClusterName] + + resources, err := b.SDK.ListStackResources(ctx, project) + if err != nil { + return nil, err + } + + servicesARN := []string{} + for _, r := range resources { + switch r.Type { + case "AWS::ECS::Service": + servicesARN = append(servicesARN, r.ARN) + case "AWS::ECS::Cluster": + cluster = r.ARN + } + } + if len(servicesARN) == 0 { + return nil, nil + } + status, err := b.SDK.DescribeServices(ctx, cluster, servicesARN) + if err != nil { + return nil, err + } + + for i, state := range status { + ports := []string{} + for _, lb := range state.Publishers { + ports = append(ports, fmt.Sprintf( + "%s:%d->%d/%s", + lb.URL, + lb.PublishedPort, + lb.TargetPort, + strings.ToLower(lb.Protocol))) + } + state.Ports = ports + status[i] = state + } + return status, nil +} diff --git a/ecs/sdk.go b/ecs/sdk.go index e0a45330f..8346ba169 100644 --- a/ecs/sdk.go +++ b/ecs/sdk.go @@ -208,6 +208,12 @@ func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template Capabilities: []*string{ aws.String(cloudformation.CapabilityCapabilityIam), }, + Tags: []*cloudformation.Tag{ + { + Key: aws.String(compose.ProjectTag), + Value: aws.String(name), + }, + }, }) return err } @@ -296,6 +302,36 @@ func (s sdk) GetStackID(ctx context.Context, name string) (string, error) { return *stacks.Stacks[0].StackId, nil } +func (s sdk) ListStacks(ctx context.Context, name string) ([]compose.Stack, error) { + cfStacks, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{}) + if err != nil { + return nil, err + } + stacks := []compose.Stack{} + for _, stack := range cfStacks.Stacks { + for _, t := range stack.Tags { + if *t.Key == compose.ProjectTag { + status := compose.RUNNING + switch aws.StringValue(stack.StackStatus) { + case "CREATE_IN_PROGRESS": + status = compose.STARTING + case "DELETE_IN_PROGRESS": + status = compose.REMOVING + case "UPDATE_IN_PROGRESS": + status = compose.UPDATING + } + stacks = append(stacks, compose.Stack{ + ID: aws.StringValue(stack.StackId), + Name: aws.StringValue(stack.StackName), + Status: status, + }) + continue + } + } + } + return stacks, nil +} + func (s sdk) DescribeStackEvents(ctx context.Context, stackID string) ([]*cloudformation.StackEvent, error) { // Fixme implement Paginator on Events and return as a chan(events) events := []*cloudformation.StackEvent{} diff --git a/example/backend.go b/example/backend.go index e184465c4..bc0d56467 100644 --- a/example/backend.go +++ b/example/backend.go @@ -139,7 +139,9 @@ func (cs *composeService) Down(ctx context.Context, project string) error { func (cs *composeService) Ps(ctx context.Context, project string) ([]compose.ServiceStatus, error) { return nil, errdefs.ErrNotImplemented } - +func (cs *composeService) List(ctx context.Context, project string) ([]compose.Stack, error) { + return nil, errdefs.ErrNotImplemented +} func (cs *composeService) Logs(ctx context.Context, project string, w io.Writer) error { return errdefs.ErrNotImplemented }