From a5e34323e2c91f42c47ee3ddc2e35896e2f483db Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Wed, 14 Oct 2020 16:06:45 +0200 Subject: [PATCH] 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)) }) }