From 7cf7b005847c588abe79e67630c2bf0b35c1794a Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Mon, 12 Oct 2020 10:18:45 +0200 Subject: [PATCH 1/2] Add ResourceService definition and ACI NOOP implementation Signed-off-by: Guillaume Tardif --- aci/backend.go | 6 ++++++ aci/resources.go | 34 ++++++++++++++++++++++++++++++++++ api/resources/api.go | 32 ++++++++++++++++++++++++++++++++ backend/backend.go | 2 ++ ecs/backend.go | 5 +++++ ecs/local/backend.go | 5 +++++ example/backend.go | 11 ++++++++--- local/backend.go | 5 +++++ server/metrics_test.go | 3 +++ 9 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 aci/resources.go create mode 100644 api/resources/api.go diff --git a/aci/backend.go b/aci/backend.go index f2198b543..02e98a2c0 100644 --- a/aci/backend.go +++ b/aci/backend.go @@ -28,6 +28,7 @@ import ( "github.com/docker/compose-cli/aci/login" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/containers" + "github.com/docker/compose-cli/api/resources" "github.com/docker/compose-cli/api/secrets" "github.com/docker/compose-cli/api/volumes" "github.com/docker/compose-cli/backend" @@ -103,6 +104,7 @@ type aciAPIService struct { *aciContainerService *aciComposeService *aciVolumeService + *aciResourceService } func (a *aciAPIService) ContainerService() containers.Service { @@ -123,6 +125,10 @@ func (a *aciAPIService) VolumeService() volumes.Service { return a.aciVolumeService } +func (a *aciAPIService) ResourceService() resources.Service { + return a.aciResourceService +} + func getContainerID(group containerinstance.ContainerGroup, container containerinstance.Container) string { containerID := *group.Name + composeContainerSeparator + *container.Name if _, ok := group.Tags[singleContainerTag]; ok { diff --git a/aci/resources.go b/aci/resources.go new file mode 100644 index 000000000..2710c3520 --- /dev/null +++ b/aci/resources.go @@ -0,0 +1,34 @@ +/* + Copyright 2020 Docker Compose CLI authors + + 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 aci + +import ( + "context" + "fmt" + + "github.com/docker/compose-cli/api/resources" + "github.com/docker/compose-cli/context/store" +) + +type aciResourceService struct { + aciContext store.AciContext +} + +func (cs *aciResourceService) Prune(ctx context.Context, request resources.PruneRequest) ([]string, error) { + fmt.Println("PRUNE " + cs.aciContext.ResourceGroup) + return nil, nil +} diff --git a/api/resources/api.go b/api/resources/api.go new file mode 100644 index 000000000..8393206a0 --- /dev/null +++ b/api/resources/api.go @@ -0,0 +1,32 @@ +/* + Copyright 2020 Docker Compose CLI authors + + 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 resources + +import ( + "context" +) + +// PruneRequest options on what to prune +type PruneRequest struct { + Force bool +} + +// Service interacts with the underlying container backend +type Service interface { + // Prune prune resources + Prune(ctx context.Context, request PruneRequest) ([]string, error) +} diff --git a/backend/backend.go b/backend/backend.go index f42df393f..8852151b8 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -25,6 +25,7 @@ import ( "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/containers" + "github.com/docker/compose-cli/api/resources" "github.com/docker/compose-cli/api/secrets" "github.com/docker/compose-cli/api/volumes" "github.com/docker/compose-cli/context/cloud" @@ -55,6 +56,7 @@ var backends = struct { type Service interface { ContainerService() containers.Service ComposeService() compose.Service + ResourceService() resources.Service SecretsService() secrets.Service VolumeService() volumes.Service } diff --git a/ecs/backend.go b/ecs/backend.go index 16d1c35d3..ab5b3ddec 100644 --- a/ecs/backend.go +++ b/ecs/backend.go @@ -24,6 +24,7 @@ import ( "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/containers" + "github.com/docker/compose-cli/api/resources" "github.com/docker/compose-cli/api/secrets" "github.com/docker/compose-cli/api/volumes" "github.com/docker/compose-cli/backend" @@ -103,6 +104,10 @@ func (a *ecsAPIService) VolumeService() volumes.Service { return nil } +func (a *ecsAPIService) ResourceService() resources.Service { + return nil +} + func getCloudService() (cloud.Service, error) { return ecsCloudService{}, nil } diff --git a/ecs/local/backend.go b/ecs/local/backend.go index f2a789626..934bf0b86 100644 --- a/ecs/local/backend.go +++ b/ecs/local/backend.go @@ -23,6 +23,7 @@ import ( "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/containers" + "github.com/docker/compose-cli/api/resources" "github.com/docker/compose-cli/api/secrets" "github.com/docker/compose-cli/api/volumes" "github.com/docker/compose-cli/backend" @@ -70,3 +71,7 @@ func (e ecsLocalSimulation) SecretsService() secrets.Service { func (e ecsLocalSimulation) ComposeService() compose.Service { return e } + +func (e ecsLocalSimulation) ResourceService() resources.Service { + return nil +} diff --git a/example/backend.go b/example/backend.go index 9f68bdbf6..8e8f1ede7 100644 --- a/example/backend.go +++ b/example/backend.go @@ -28,6 +28,7 @@ import ( "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/containers" + "github.com/docker/compose-cli/api/resources" "github.com/docker/compose-cli/api/secrets" "github.com/docker/compose-cli/api/volumes" "github.com/docker/compose-cli/backend" @@ -56,6 +57,10 @@ func (a *apiService) VolumeService() volumes.Service { return nil } +func (a *apiService) ResourceService() resources.Service { + return nil +} + func init() { backend.Register("example", "example", service, cloud.NotImplementedCloudService) } @@ -68,9 +73,9 @@ type containerService struct{} func (cs *containerService) Inspect(ctx context.Context, id string) (containers.Container, error) { return containers.Container{ - ID: "id", - Image: "nginx", - Platform: "Linux", + ID: "id", + Image: "nginx", + Platform: "Linux", HostConfig: &containers.HostConfig{ RestartPolicy: "none", }, diff --git a/local/backend.go b/local/backend.go index c616715b6..baaf7e4fa 100644 --- a/local/backend.go +++ b/local/backend.go @@ -38,6 +38,7 @@ import ( "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/containers" + "github.com/docker/compose-cli/api/resources" "github.com/docker/compose-cli/api/secrets" "github.com/docker/compose-cli/api/volumes" "github.com/docker/compose-cli/backend" @@ -80,6 +81,10 @@ func (ms *local) VolumeService() volumes.Service { return nil } +func (ms *local) ResourceService() resources.Service { + return nil +} + func (ms *local) Inspect(ctx context.Context, id string) (containers.Container, error) { c, err := ms.apiClient.ContainerInspect(ctx, id) if err != nil { diff --git a/server/metrics_test.go b/server/metrics_test.go index 659634c3f..9c2752308 100644 --- a/server/metrics_test.go +++ b/server/metrics_test.go @@ -21,6 +21,8 @@ import ( "strings" "testing" + "github.com/docker/compose-cli/api/resources" + "github.com/stretchr/testify/mock" "google.golang.org/grpc" "google.golang.org/grpc/metadata" @@ -116,6 +118,7 @@ func (noopService) ContainerService() containers.Service { return nil } func (noopService) ComposeService() compose.Service { return nil } func (noopService) SecretsService() secrets.Service { return nil } func (noopService) VolumeService() volumes.Service { return nil } +func (noopService) ResourceService() resources.Service { return nil } type mockMetricsClient struct { mock.Mock From a5e34323e2c91f42c47ee3ddc2e35896e2f483db Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Wed, 14 Oct 2020 16:06:45 +0200 Subject: [PATCH 2/2] Add `docker prune` command and ACI implementation Signed-off-by: Guillaume Tardif --- aci/backend.go | 3 ++ aci/convert/convert.go | 13 +++++-- aci/resources.go | 26 +++++++++++-- api/client/client.go | 10 +++++ api/client/resources.go | 32 ++++++++++++++++ api/resources/api.go | 3 +- cli/cmd/prune.go | 69 +++++++++++++++++++++++++++++++++++ cli/main.go | 1 + tests/aci-e2e/e2e-aci_test.go | 26 ++++++++++--- 9 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 api/client/resources.go create mode 100644 cli/cmd/prune.go diff --git a/aci/backend.go b/aci/backend.go index 02e98a2c0..0aeffb091 100644 --- a/aci/backend.go +++ b/aci/backend.go @@ -97,6 +97,9 @@ func getAciAPIService(aciCtx store.AciContext) *aciAPIService { aciVolumeService: &aciVolumeService{ aciContext: aciCtx, }, + aciResourceService: &aciResourceService{ + aciContext: aciCtx, + }, } } diff --git a/aci/convert/convert.go b/aci/convert/convert.go index 28388c4c4..f67168b95 100644 --- a/aci/convert/convert.go +++ b/aci/convert/convert.go @@ -542,12 +542,17 @@ func ContainerGroupToContainer(containerID string, cg containerinstance.Containe // GetStatus returns status for the specified container func GetStatus(container containerinstance.Container, group containerinstance.ContainerGroup) string { - status := compose.UNKNOWN - if group.InstanceView != nil && group.InstanceView.State != nil { - status = "Node " + *group.InstanceView.State - } + status := GetGroupStatus(group) if container.InstanceView != nil && container.InstanceView.CurrentState != nil { status = *container.InstanceView.CurrentState.State } return status } + +// GetGroupStatus returns status for the container group +func GetGroupStatus(group containerinstance.ContainerGroup) string { + if group.InstanceView != nil && group.InstanceView.State != nil { + return "Node " + *group.InstanceView.State + } + return compose.UNKNOWN +} diff --git a/aci/resources.go b/aci/resources.go index 2710c3520..e1beff203 100644 --- a/aci/resources.go +++ b/aci/resources.go @@ -18,8 +18,10 @@ package aci import ( "context" - "fmt" + "github.com/hashicorp/go-multierror" + + "github.com/docker/compose-cli/aci/convert" "github.com/docker/compose-cli/api/resources" "github.com/docker/compose-cli/context/store" ) @@ -29,6 +31,24 @@ type aciResourceService struct { } func (cs *aciResourceService) Prune(ctx context.Context, request resources.PruneRequest) ([]string, error) { - fmt.Println("PRUNE " + cs.aciContext.ResourceGroup) - return nil, nil + res, err := getACIContainerGroups(ctx, cs.aciContext.SubscriptionID, cs.aciContext.ResourceGroup) + if err != nil { + return nil, err + } + multierr := &multierror.Error{} + deleted := []string{} + for _, containerGroup := range res { + if !request.Force && convert.GetGroupStatus(containerGroup) == "Node "+convert.StatusRunning { + continue + } + + if !request.DryRun { + _, err := deleteACIContainerGroup(ctx, cs.aciContext, *containerGroup.Name) + multierr = multierror.Append(multierr, err) + } + if err == nil { + deleted = append(deleted, *containerGroup.Name) + } + } + return deleted, multierr.ErrorOrNil() } diff --git a/api/client/client.go b/api/client/client.go index 9dfb2f627..ffcdad19e 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -21,6 +21,7 @@ import ( "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/containers" + "github.com/docker/compose-cli/api/resources" "github.com/docker/compose-cli/api/secrets" "github.com/docker/compose-cli/api/volumes" "github.com/docker/compose-cli/backend" @@ -107,3 +108,12 @@ func (c *Client) VolumeService() volumes.Service { return &volumeService{} } + +// ResourceService returns the backend service for the current context +func (c *Client) ResourceService() resources.Service { + if vs := c.bs.ResourceService(); vs != nil { + return vs + } + + return &resourceService{} +} diff --git a/api/client/resources.go b/api/client/resources.go new file mode 100644 index 000000000..74eb08733 --- /dev/null +++ b/api/client/resources.go @@ -0,0 +1,32 @@ +/* + Copyright 2020 Docker Compose CLI authors + + 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 client + +import ( + "context" + + "github.com/docker/compose-cli/api/resources" + "github.com/docker/compose-cli/errdefs" +) + +type resourceService struct { +} + +// Prune prune resources +func (c *resourceService) Prune(ctx context.Context, request resources.PruneRequest) ([]string, error) { + return nil, errdefs.ErrNotImplemented +} diff --git a/api/resources/api.go b/api/resources/api.go index 8393206a0..f3f6f1841 100644 --- a/api/resources/api.go +++ b/api/resources/api.go @@ -22,7 +22,8 @@ import ( // PruneRequest options on what to prune type PruneRequest struct { - Force bool + Force bool + DryRun bool } // Service interacts with the underlying container backend diff --git a/cli/cmd/prune.go b/cli/cmd/prune.go new file mode 100644 index 000000000..b0a61b370 --- /dev/null +++ b/cli/cmd/prune.go @@ -0,0 +1,69 @@ +/* + Copyright 2020 Docker Compose CLI authors + + 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 cmd + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/docker/compose-cli/api/client" + "github.com/docker/compose-cli/api/resources" +) + +type pruneOpts struct { + force bool + dryRun bool +} + +// PruneCommand deletes backend resources +func PruneCommand() *cobra.Command { + var opts pruneOpts + cmd := &cobra.Command{ + Use: "prune", + Short: "prune existing resources in current context", + Args: cobra.MaximumNArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return runPrune(cmd.Context(), opts) + }, + } + + cmd.Flags().BoolVar(&opts.force, "force", false, "Also prune running containers and Compose applications") + cmd.Flags().BoolVar(&opts.dryRun, "dry-run", false, "List resources to be deleted, but do not delete them") + + return cmd +} + +func runPrune(ctx context.Context, opts pruneOpts) error { + c, err := client.New(ctx) + if err != nil { + return errors.Wrap(err, "cannot connect to backend") + } + + ids, err := c.ResourceService().Prune(ctx, resources.PruneRequest{Force: opts.force, DryRun: opts.dryRun}) + if opts.dryRun { + fmt.Println("resources that would be deleted:") + } else { + fmt.Println("deleted resources:") + } + for _, id := range ids { + fmt.Println(id) + } + return err +} diff --git a/cli/main.go b/cli/main.go index 2de093e1c..7b70d887a 100644 --- a/cli/main.go +++ b/cli/main.go @@ -122,6 +122,7 @@ func main() { cmd.StopCommand(), cmd.KillCommand(), cmd.SecretCommand(), + cmd.PruneCommand(), // Place holders cmd.EcsCommand(), diff --git a/tests/aci-e2e/e2e-aci_test.go b/tests/aci-e2e/e2e-aci_test.go index 198cba625..ce806070a 100644 --- a/tests/aci-e2e/e2e-aci_test.go +++ b/tests/aci-e2e/e2e-aci_test.go @@ -488,13 +488,27 @@ func TestContainerRunAttached(t *testing.T) { waitForStatus(t, c, container, convert.StatusRunning) }) - t.Run("kill & rm stopped container", func(t *testing.T) { - res := c.RunDockerCmd("kill", container) - res.Assert(t, icmd.Expected{Out: container}) - waitForStatus(t, c, container, "Terminated", "Node Stopped") + t.Run("prune dry run", func(t *testing.T) { + res := c.RunDockerCmd("prune", "--dry-run") + fmt.Println("prune output:") + assert.Equal(t, "resources that would be deleted:\n", res.Stdout()) + res = c.RunDockerCmd("prune", "--dry-run", "--force") + assert.Equal(t, "resources that would be deleted:\n"+container+"\n", res.Stdout()) + }) - res = c.RunDockerCmd("rm", container) - res.Assert(t, icmd.Expected{Out: container}) + t.Run("prune", func(t *testing.T) { + res := c.RunDockerCmd("prune") + assert.Equal(t, "deleted resources:\n", res.Stdout()) + res = c.RunDockerCmd("ps") + l := lines(res.Stdout()) + assert.Equal(t, 2, len(l)) + + res = c.RunDockerCmd("prune", "--force") + assert.Equal(t, "deleted resources:\n"+container+"\n", res.Stdout()) + + res = c.RunDockerCmd("ps", "--all") + l = lines(res.Stdout()) + assert.Equal(t, 1, len(l)) }) }