Browse Source

Merge pull request #1505 from aiordache/image_cmd

Add `compose images` cmd
Guillaume Tardif 4 years ago
parent
commit
eb7732ffec

+ 4 - 0
aci/compose.go

@@ -241,3 +241,7 @@ func (cs *aciComposeService) Events(ctx context.Context, project string, options
 func (cs *aciComposeService) Port(ctx context.Context, project string, service string, port int, options compose.PortOptions) (string, int, error) {
 	return "", 0, errdefs.ErrNotImplemented
 }
+
+func (cs *aciComposeService) Images(ctx context.Context, projectName string, options compose.ImagesOptions) ([]compose.ImageSummary, error) {
+	return nil, errdefs.ErrNotImplemented
+}

+ 4 - 0
api/client/compose.go

@@ -115,3 +115,7 @@ func (c *composeService) Events(ctx context.Context, project string, options com
 func (c *composeService) Port(ctx context.Context, project string, service string, port int, options compose.PortOptions) (string, int, error) {
 	return "", 0, errdefs.ErrNotImplemented
 }
+
+func (c *composeService) Images(ctx context.Context, projectName string, options compose.ImagesOptions) ([]compose.ImageSummary, error) {
+	return nil, errdefs.ErrNotImplemented
+}

+ 16 - 0
api/compose/api.go

@@ -72,6 +72,8 @@ type Service interface {
 	Events(ctx context.Context, project string, options EventsOptions) error
 	// Port executes the equivalent to a `compose port`
 	Port(ctx context.Context, project string, service string, port int, options PortOptions) (string, int, error)
+	// Images executes the equivalent of a `compose images`
+	Images(ctx context.Context, projectName string, options ImagesOptions) ([]ImageSummary, error)
 }
 
 // BuildOptions group options of the Build API
@@ -164,6 +166,11 @@ type PullOptions struct {
 	IgnoreFailures bool
 }
 
+// ImagesOptions group options of the Images API
+type ImagesOptions struct {
+	Services []string
+}
+
 // KillOptions group options of the Kill API
 type KillOptions struct {
 	// Signal to send to containers
@@ -285,6 +292,15 @@ type ContainerProcSummary struct {
 	Titles    []string
 }
 
+// ImageSummary holds container image description
+type ImageSummary struct {
+	ID            string
+	ContainerName string
+	Repository    string
+	Tag           string
+	Size          int64
+}
+
 // ServiceStatus hold status about a service
 type ServiceStatus struct {
 	ID         string

+ 1 - 0
cli/cmd/compose/compose.go

@@ -157,6 +157,7 @@ func Command(contextType string) *cobra.Command {
 		topCommand(&opts),
 		eventsCommand(&opts),
 		portCommand(&opts),
+		imagesCommand(&opts),
 	)
 
 	if contextType == store.LocalContextType || contextType == store.DefaultContextType {

+ 107 - 0
cli/cmd/compose/images.go

@@ -0,0 +1,107 @@
+/*
+   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 compose
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"sort"
+	"strings"
+
+	"github.com/spf13/cobra"
+
+	"github.com/docker/compose-cli/api/client"
+	"github.com/docker/compose-cli/api/compose"
+	"github.com/docker/compose-cli/cli/formatter"
+	"github.com/docker/compose-cli/utils"
+	"github.com/docker/docker/pkg/stringid"
+
+	units "github.com/docker/go-units"
+)
+
+type imageOptions struct {
+	*projectOptions
+	Quiet bool
+}
+
+func imagesCommand(p *projectOptions) *cobra.Command {
+	opts := imageOptions{
+		projectOptions: p,
+	}
+	imgCmd := &cobra.Command{
+		Use:   "images [SERVICE...]",
+		Short: "List images used by the created containers",
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runImages(cmd.Context(), opts, args)
+		},
+	}
+	imgCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
+	return imgCmd
+}
+
+func runImages(ctx context.Context, opts imageOptions, services []string) error {
+	c, err := client.New(ctx)
+	if err != nil {
+		return err
+	}
+
+	projectName, err := opts.toProjectName()
+	if err != nil {
+		return err
+	}
+
+	images, err := c.ComposeService().Images(ctx, projectName, compose.ImagesOptions{
+		Services: services,
+	})
+	if err != nil {
+		return err
+	}
+
+	if opts.Quiet {
+		ids := []string{}
+		for _, img := range images {
+			id := img.ID
+			if i := strings.IndexRune(img.ID, ':'); i >= 0 {
+				id = id[i+1:]
+			}
+			if !utils.StringContains(ids, id) {
+				ids = append(ids, id)
+			}
+		}
+		for _, img := range ids {
+			fmt.Println(img)
+		}
+		return nil
+	}
+
+	sort.Slice(images, func(i, j int) bool {
+		return images[i].ContainerName < images[j].ContainerName
+	})
+
+	return formatter.Print(images, formatter.PRETTY, os.Stdout,
+		func(w io.Writer) {
+			for _, img := range images {
+				id := stringid.TruncateID(img.ID)
+				size := units.HumanSizeWithPrecision(float64(img.Size), 3)
+
+				_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", img.ContainerName, img.Repository, img.Tag, id, size)
+			}
+		},
+		"Container", "Repository", "Tag", "Image Id", "Size")
+}

+ 28 - 0
ecs/images.go

@@ -0,0 +1,28 @@
+/*
+   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 ecs
+
+import (
+	"context"
+
+	"github.com/docker/compose-cli/api/compose"
+	"github.com/docker/compose-cli/api/errdefs"
+)
+
+func (b *ecsAPIService) Images(ctx context.Context, projectName string, options compose.ImagesOptions) ([]compose.ImageSummary, error) {
+	return nil, errdefs.ErrNotImplemented
+}

+ 4 - 0
ecs/local/compose.go

@@ -207,3 +207,7 @@ func (e ecsLocalSimulation) Events(ctx context.Context, project string, options
 func (e ecsLocalSimulation) Port(ctx context.Context, project string, service string, port int, options compose.PortOptions) (string, int, error) {
 	return "", 0, errdefs.ErrNotImplemented
 }
+
+func (e ecsLocalSimulation) Images(ctx context.Context, projectName string, options compose.ImagesOptions) ([]compose.ImageSummary, error) {
+	return nil, errdefs.ErrNotImplemented
+}

+ 4 - 0
kube/compose.go

@@ -271,3 +271,7 @@ func (s *composeService) Events(ctx context.Context, project string, options com
 func (s *composeService) Port(ctx context.Context, project string, service string, port int, options compose.PortOptions) (string, int, error) {
 	return "", 0, errdefs.ErrNotImplemented
 }
+
+func (s *composeService) Images(ctx context.Context, projectName string, options compose.ImagesOptions) ([]compose.ImageSummary, error) {
+	return nil, errdefs.ErrNotImplemented
+}

+ 104 - 0
local/compose/images.go

@@ -0,0 +1,104 @@
+/*
+   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 compose
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	moby "github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"golang.org/x/sync/errgroup"
+
+	"github.com/docker/compose-cli/api/compose"
+	"github.com/docker/compose-cli/utils"
+)
+
+func (s *composeService) Images(ctx context.Context, projectName string, options compose.ImagesOptions) ([]compose.ImageSummary, error) {
+	allContainers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
+		Filters: filters.NewArgs(projectFilter(projectName)),
+	})
+	if err != nil {
+		return nil, err
+	}
+	containers := []moby.Container{}
+	if len(options.Services) > 0 {
+		// filter service containers
+		for _, c := range allContainers {
+			if utils.StringContains(options.Services, c.Labels[compose.ServiceTag]) {
+				containers = append(containers, c)
+
+			}
+		}
+	} else {
+		containers = allContainers
+	}
+
+	imageIDs := []string{}
+	// aggregate image IDs
+	for _, c := range containers {
+		if !utils.StringContains(imageIDs, c.ImageID) {
+			imageIDs = append(imageIDs, c.ImageID)
+		}
+	}
+
+	images := map[string]moby.ImageInspect{}
+	eg, ctx := errgroup.WithContext(ctx)
+	for _, img := range imageIDs {
+		img := img
+		eg.Go(func() error {
+			inspect, _, err := s.apiClient.ImageInspectWithRaw(ctx, img)
+			if err != nil {
+				return err
+			}
+			images[img] = inspect
+			return nil
+		})
+	}
+	err = eg.Wait()
+
+	if err != nil {
+		return nil, err
+	}
+	summary := make([]compose.ImageSummary, len(containers))
+	for i, container := range containers {
+		img, ok := images[container.ImageID]
+		if !ok {
+			return nil, fmt.Errorf("failed to retrieve image for container %s", getCanonicalContainerName(container))
+		}
+		if len(img.RepoTags) == 0 {
+			return nil, fmt.Errorf("no image tag found for %s", img.ID)
+		}
+		tag := ""
+		repository := ""
+		repotag := strings.Split(img.RepoTags[0], ":")
+		repository = repotag[0]
+		if len(repotag) > 1 {
+			tag = repotag[1]
+		}
+
+		summary[i] = compose.ImageSummary{
+			ID:            img.ID,
+			ContainerName: getCanonicalContainerName(container),
+			Repository:    repository,
+			Tag:           tag,
+			Size:          img.Size,
+		}
+	}
+	return summary, nil
+}

+ 7 - 0
local/e2e/compose/compose_test.go

@@ -109,6 +109,13 @@ func TestLocalComposeUp(t *testing.T) {
 		res.Assert(t, icmd.Expected{Out: `compose-e2e-demo_db_1      db                  running             5432/tcp`})
 	})
 
+	t.Run("images", func(t *testing.T) {
+		res := c.RunDockerCmd("compose", "-p", projectName, "images")
+		res.Assert(t, icmd.Expected{Out: `compose-e2e-demo_db_1      gtardif/sentences-db    latest`})
+		res.Assert(t, icmd.Expected{Out: `compose-e2e-demo_web_1     gtardif/sentences-web   latest`})
+		res.Assert(t, icmd.Expected{Out: `compose-e2e-demo_words_1   gtardif/sentences-api   latest`})
+	})
+
 	t.Run("down", func(t *testing.T) {
 		_ = c.RunDockerCmd("compose", "--project-name", projectName, "down")
 	})