introduce config --lock-image-digests

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2025-05-16 19:37:43 +02:00 committed by Guillaume Lours
parent 1f076a3781
commit 2352a4a016
4 changed files with 54 additions and 5 deletions

View File

@ -56,6 +56,7 @@ type configOptions struct {
noConsistency bool noConsistency bool
variables bool variables bool
environment bool environment bool
lockImageDigests bool
} }
func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, error) { 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 { if p.Compatibility {
opts.noNormalize = true opts.noNormalize = true
} }
if opts.lockImageDigests {
opts.resolveImageDigests = true
}
return nil return nil
}), }),
RunE: Adapt(func(ctx context.Context, args []string) error { 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 := cmd.Flags()
flags.StringVar(&opts.Format, "format", "", "Format the output. Values: [yaml | json]") 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.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.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.noInterpolate, "no-interpolate", false, "Don't interpolate environment variables")
flags.BoolVar(&opts.noNormalize, "no-normalize", false, "Don't normalize compose model") 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 var content []byte
switch opts.Format { switch opts.Format {
case "json": case "json":
@ -223,6 +232,18 @@ func runConfigInterpolate(ctx context.Context, dockerCli command.Cli, opts confi
return content, nil 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) { 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 // we can't use ToProject, so the model we render here is only partially resolved
model, err := opts.ToModel(ctx, dockerCli, services) 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) return formatModel(model, opts.Format)
} }

View File

@ -14,6 +14,7 @@ the canonical format.
| `--format` | `string` | | Format the output. Values: [yaml \| json] | | `--format` | `string` | | Format the output. Values: [yaml \| json] |
| `--hash` | `string` | | Print the service config hash, one per line. | | `--hash` | `string` | | Print the service config hash, one per line. |
| `--images` | `bool` | | Print the image names, 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-consistency` | `bool` | | Don't check model consistency - warning: may produce invalid Compose output |
| `--no-env-resolution` | `bool` | | Don't resolve service env files | | `--no-env-resolution` | `bool` | | Don't resolve service env files |
| `--no-interpolate` | `bool` | | Don't interpolate environment variables | | `--no-interpolate` | `bool` | | Don't interpolate environment variables |

View File

@ -46,6 +46,16 @@ options:
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
swarm: 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 - option: no-consistency
value_type: bool value_type: bool
default_value: "false" default_value: "false"

View File

@ -235,7 +235,7 @@ func TestCompatibility(t *testing.T) {
} }
func TestConfig(t *testing.T) { func TestConfig(t *testing.T) {
const projectName = "compose-e2e-convert" const projectName = "compose-e2e-config"
c := NewParallelCLI(t) c := NewParallelCLI(t)
wd, err := os.Getwd() wd, err := os.Getwd()
@ -253,24 +253,24 @@ services:
default: null default: null
networks: networks:
default: default:
name: compose-e2e-convert_default name: compose-e2e-config_default
`, projectName, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0}) `, projectName, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0})
}) })
} }
func TestConfigInterpolate(t *testing.T) { func TestConfigInterpolate(t *testing.T) {
const projectName = "compose-e2e-convert-interpolate" const projectName = "compose-e2e-config-interpolate"
c := NewParallelCLI(t) c := NewParallelCLI(t)
wd, err := os.Getwd() wd, err := os.Getwd()
assert.NilError(t, err) 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 := 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 res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`name: %s
networks: networks:
default: default:
name: compose-e2e-convert-interpolate_default name: compose-e2e-config-interpolate_default
services: services:
nginx: nginx:
build: build: