mirror of https://github.com/docker/compose.git
				
				
				
			Merge pull request #1505 from aiordache/image_cmd
Add `compose images` cmd
This commit is contained in:
		
						commit
						eb7732ffec
					
				|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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") | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  | @ -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") | ||||
| 	}) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue