From 2352a4a01637c2d78ecbf533efb4b74cb0e51ea8 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Fri, 16 May 2025 19:37:43 +0200 Subject: [PATCH] introduce config --lock-image-digests Signed-off-by: Nicolas De Loof --- cmd/compose/config.go | 38 +++++++++++++++++++++++ docs/reference/compose_config.md | 1 + docs/reference/docker_compose_config.yaml | 10 ++++++ pkg/e2e/compose_test.go | 10 +++--- 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/cmd/compose/config.go b/cmd/compose/config.go index e59fcc2d4..3cb969108 100644 --- a/cmd/compose/config.go +++ b/cmd/compose/config.go @@ -56,6 +56,7 @@ type configOptions struct { noConsistency bool variables bool environment bool + lockImageDigests bool } func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, error) { @@ -98,6 +99,9 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command { if p.Compatibility { opts.noNormalize = true } + if opts.lockImageDigests { + opts.resolveImageDigests = true + } return nil }), RunE: Adapt(func(ctx context.Context, args []string) error { @@ -133,6 +137,7 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.StringVar(&opts.Format, "format", "", "Format the output. Values: [yaml | json]") flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests") + flags.BoolVar(&opts.lockImageDigests, "lock-image-digests", false, "Produces an override file with image digests") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only validate the configuration, don't print anything") flags.BoolVar(&opts.noInterpolate, "no-interpolate", false, "Don't interpolate environment variables") flags.BoolVar(&opts.noNormalize, "no-normalize", false, "Don't normalize compose model") @@ -208,6 +213,10 @@ func runConfigInterpolate(ctx context.Context, dockerCli command.Cli, opts confi } } + if opts.lockImageDigests { + project = imagesOnly(project) + } + var content []byte switch opts.Format { case "json": @@ -223,6 +232,18 @@ func runConfigInterpolate(ctx context.Context, dockerCli command.Cli, opts confi return content, nil } +// imagesOnly return project with all attributes removed but service.images +func imagesOnly(project *types.Project) *types.Project { + digests := types.Services{} + for name, config := range project.Services { + digests[name] = types.ServiceConfig{ + Image: config.Image, + } + } + project = &types.Project{Services: digests} + return project +} + func runConfigNoInterpolate(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) ([]byte, error) { // we can't use ToProject, so the model we render here is only partially resolved model, err := opts.ToModel(ctx, dockerCli, services) @@ -237,6 +258,23 @@ func runConfigNoInterpolate(ctx context.Context, dockerCli command.Cli, opts con } } + if opts.lockImageDigests { + for key, e := range model { + if key != "services" { + delete(model, key) + } else { + for _, s := range e.(map[string]any) { + service := s.(map[string]any) + for key := range service { + if key != "image" { + delete(service, key) + } + } + } + } + } + } + return formatModel(model, opts.Format) } diff --git a/docs/reference/compose_config.md b/docs/reference/compose_config.md index ee25c1520..3ec2d4864 100644 --- a/docs/reference/compose_config.md +++ b/docs/reference/compose_config.md @@ -14,6 +14,7 @@ the canonical format. | `--format` | `string` | | Format the output. Values: [yaml \| json] | | `--hash` | `string` | | Print the service config hash, one per line. | | `--images` | `bool` | | Print the image names, one per line. | +| `--lock-image-digests` | `bool` | | Produces an override file with image digests | | `--no-consistency` | `bool` | | Don't check model consistency - warning: may produce invalid Compose output | | `--no-env-resolution` | `bool` | | Don't resolve service env files | | `--no-interpolate` | `bool` | | Don't interpolate environment variables | diff --git a/docs/reference/docker_compose_config.yaml b/docs/reference/docker_compose_config.yaml index 2f0f9184b..080fe6748 100644 --- a/docs/reference/docker_compose_config.yaml +++ b/docs/reference/docker_compose_config.yaml @@ -46,6 +46,16 @@ options: experimentalcli: false kubernetes: false swarm: false + - option: lock-image-digests + value_type: bool + default_value: "false" + description: Produces an override file with image digests + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false - option: no-consistency value_type: bool default_value: "false" diff --git a/pkg/e2e/compose_test.go b/pkg/e2e/compose_test.go index 78f6ea370..3b5a7341a 100644 --- a/pkg/e2e/compose_test.go +++ b/pkg/e2e/compose_test.go @@ -235,7 +235,7 @@ func TestCompatibility(t *testing.T) { } func TestConfig(t *testing.T) { - const projectName = "compose-e2e-convert" + const projectName = "compose-e2e-config" c := NewParallelCLI(t) wd, err := os.Getwd() @@ -253,24 +253,24 @@ services: default: null networks: default: - name: compose-e2e-convert_default + name: compose-e2e-config_default `, projectName, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0}) }) } func TestConfigInterpolate(t *testing.T) { - const projectName = "compose-e2e-convert-interpolate" + const projectName = "compose-e2e-config-interpolate" c := NewParallelCLI(t) wd, err := os.Getwd() assert.NilError(t, err) - t.Run("convert", func(t *testing.T) { + t.Run("config", func(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-build-test/compose-interpolate.yaml", "-p", projectName, "config", "--no-interpolate") res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`name: %s networks: default: - name: compose-e2e-convert-interpolate_default + name: compose-e2e-config-interpolate_default services: nginx: build: