diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index fc29014c4..8e9d52599 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -201,6 +201,24 @@ func (o *ProjectOptions) toProjectName(ctx context.Context, dockerCli command.Cl return project.Name, nil } +func (o *ProjectOptions) ToModel(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (map[string]any, error) { + remotes := o.remoteLoaders(dockerCli) + for _, r := range remotes { + po = append(po, cli.WithResourceLoader(r)) + } + + options, err := o.toProjectOptions(po...) + if err != nil { + return nil, err + } + + if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) { + api.Separator = "_" + } + + return options.LoadModel(ctx) +} + func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, tracing.Metrics, error) { var metrics tracing.Metrics @@ -241,7 +259,7 @@ func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, s api.Separator = "_" } - project, err := cli.ProjectFromOptions(ctx, options) + project, err := options.LoadProject(ctx) if err != nil { return nil, metrics, compose.WrapComposeError(err) } @@ -468,7 +486,7 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { // psCommand(&opts, dockerCli, backend), listCommand(dockerCli, backend), logsCommand(&opts, dockerCli, backend), - configCommand(&opts, dockerCli, backend), + configCommand(&opts, dockerCli), killCommand(&opts, dockerCli, backend), runCommand(&opts, dockerCli, backend), removeCommand(&opts, dockerCli, backend), diff --git a/cmd/compose/config.go b/cmd/compose/config.go index 481ad776d..f9176ec7b 100644 --- a/cmd/compose/config.go +++ b/cmd/compose/config.go @@ -19,6 +19,7 @@ package compose import ( "bytes" "context" + "encoding/json" "fmt" "os" "sort" @@ -28,8 +29,8 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/docker/cli/cli/command" "github.com/spf13/cobra" + "gopkg.in/yaml.v3" - "github.com/docker/compose/v2/internal/tracing" "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/compose" ) @@ -51,18 +52,29 @@ type configOptions struct { noConsistency bool } -func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, tracing.Metrics, error) { - po = append(po, +func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, error) { + po = append(po, o.ToProjectOptions()...) + project, _, err := o.ProjectOptions.ToProject(ctx, dockerCli, services, po...) + return project, err +} + +func (o *configOptions) ToModel(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (map[string]any, error) { + po = append(po, o.ToProjectOptions()...) + return o.ProjectOptions.ToModel(ctx, dockerCli, services, po...) +} + +func (o *configOptions) ToProjectOptions() []cli.ProjectOptionsFn { + return []cli.ProjectOptionsFn{ cli.WithInterpolation(!o.noInterpolate), cli.WithResolvedPaths(!o.noResolvePath), cli.WithNormalization(!o.noNormalize), cli.WithConsistency(!o.noConsistency), cli.WithDefaultProfiles(o.Profiles...), - cli.WithDiscardEnvFile) - return o.ProjectOptions.ToProject(ctx, dockerCli, services, po...) + cli.WithDiscardEnvFile, + } } -func configCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { +func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command { opts := configOptions{ ProjectOptions: p, } @@ -100,7 +112,7 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service return runConfigImages(ctx, dockerCli, opts, args) } - return runConfig(ctx, dockerCli, backend, opts, args) + return runConfig(ctx, dockerCli, opts, args) }), ValidArgsFunction: completeServiceNames(dockerCli, p), } @@ -123,18 +135,56 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service return cmd } -func runConfig(ctx context.Context, dockerCli command.Cli, backend api.Service, opts configOptions, services []string) error { +func runConfig(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error { var content []byte - project, _, err := opts.ToProject(ctx, dockerCli, services) + model, err := opts.ToModel(ctx, dockerCli, services) if err != nil { return err } - content, err = backend.Config(ctx, project, api.ConfigOptions{ - Format: opts.Format, - Output: opts.Output, - ResolveImageDigests: opts.resolveImageDigests, - }) + if opts.resolveImageDigests { + // create a pseudo-project so we can rely on WithImagesResolved to resolve images + p := &types.Project{ + Services: types.Services{}, + } + services := model["services"].(map[string]any) + for name, s := range services { + service := s.(map[string]any) + if image, ok := service["image"]; ok { + p.Services[name] = types.ServiceConfig{ + Image: image.(string), + } + } + } + + p, err = p.WithImagesResolved(compose.ImageDigestResolver(ctx, dockerCli.ConfigFile(), dockerCli.Client())) + if err != nil { + return err + } + + for name, s := range services { + service := s.(map[string]any) + config := p.Services[name] + if config.Image != "" { + service["image"] = config.Image + } + services[name] = service + } + model["services"] = services + } + + switch opts.Format { + case "json": + content, err = json.MarshalIndent(model, "", " ") + case "yaml": + buf := bytes.NewBuffer([]byte{}) + encoder := yaml.NewEncoder(buf) + encoder.SetIndent(2) + err = encoder.Encode(model) + content = buf.Bytes() + default: + return fmt.Errorf("unsupported format %q", opts.Format) + } if err != nil { return err } @@ -155,7 +205,7 @@ func runConfig(ctx context.Context, dockerCli command.Cli, backend api.Service, } func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions) error { - project, _, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution) + project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution) if err != nil { return err } @@ -167,7 +217,7 @@ func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions) } func runVolumes(ctx context.Context, dockerCli command.Cli, opts configOptions) error { - project, _, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution) + project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution) if err != nil { return err } @@ -182,7 +232,7 @@ func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) err if opts.hash != "*" { services = append(services, strings.Split(opts.hash, ",")...) } - project, _, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution) + project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution) if err != nil { return err } @@ -218,7 +268,7 @@ func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) err func runProfiles(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error { set := map[string]struct{}{} - project, _, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution) + project, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution) if err != nil { return err } @@ -239,7 +289,7 @@ func runProfiles(ctx context.Context, dockerCli command.Cli, opts configOptions, } func runConfigImages(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error { - project, _, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution) + project, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution) if err != nil { return err } diff --git a/go.mod b/go.mod index 05fa0fbfa..65d2ed7a2 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/Microsoft/go-winio v0.6.1 github.com/buger/goterm v1.0.4 - github.com/compose-spec/compose-go/v2 v2.0.0-rc.8 + github.com/compose-spec/compose-go/v2 v2.0.0-rc.8.0.20240228111658-a0507e98fe60 github.com/containerd/console v1.0.3 github.com/containerd/containerd v1.7.12 github.com/davecgh/go-spew v1.1.1 @@ -49,6 +49,7 @@ require ( golang.org/x/sync v0.6.0 golang.org/x/sys v0.16.0 google.golang.org/grpc v1.59.0 + gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.1 ) @@ -170,7 +171,6 @@ require ( google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.26.7 // indirect k8s.io/apimachinery v0.26.7 // indirect k8s.io/apiserver v0.26.7 // indirect diff --git a/go.sum b/go.sum index 22d96bb9b..adabccef1 100644 --- a/go.sum +++ b/go.sum @@ -86,8 +86,8 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+g github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= -github.com/compose-spec/compose-go/v2 v2.0.0-rc.8 h1:b7l+GqFF+2W4M4kLQUDRTGhqmTiRwT3bYd9X7xrxp5Q= -github.com/compose-spec/compose-go/v2 v2.0.0-rc.8/go.mod h1:bEPizBkIojlQ20pi2vNluBa58tevvj0Y18oUSHPyfdc= +github.com/compose-spec/compose-go/v2 v2.0.0-rc.8.0.20240228111658-a0507e98fe60 h1:NlkpaLBPFr05mNJWVMH7PP4L30gFG6k4z1QpypLUSh8= +github.com/compose-spec/compose-go/v2 v2.0.0-rc.8.0.20240228111658-a0507e98fe60/go.mod h1:bEPizBkIojlQ20pi2vNluBa58tevvj0Y18oUSHPyfdc= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= diff --git a/pkg/api/api.go b/pkg/api/api.go index 3b8f09161..1a4c9923a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -52,8 +52,6 @@ type Service interface { Ps(ctx context.Context, projectName string, options PsOptions) ([]ContainerSummary, error) // List executes the equivalent to a `docker stack ls` List(ctx context.Context, options ListOptions) ([]Stack, error) - // Config executes the equivalent to a `compose config` - Config(ctx context.Context, project *types.Project, options ConfigOptions) ([]byte, error) // Kill executes the equivalent to a `compose kill` Kill(ctx context.Context, projectName string, options KillOptions) error // RunOneOffContainer creates a service oneoff container and starts its dependencies diff --git a/pkg/compose/compose.go b/pkg/compose/compose.go index f1db51123..a025b9d90 100644 --- a/pkg/compose/compose.go +++ b/pkg/compose/compose.go @@ -25,12 +25,10 @@ import ( "strings" "sync" + "github.com/docker/docker/api/types/volume" "github.com/jonboulle/clockwork" - "github.com/docker/docker/api/types/volume" - "github.com/compose-spec/compose-go/v2/types" - "github.com/distribution/reference" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/flags" @@ -40,7 +38,6 @@ import ( "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" - "github.com/opencontainers/go-digest" ) var stdioToStdout bool @@ -147,35 +144,6 @@ func getContainerNameWithoutProject(c moby.Container) string { return name[len(project)+1:] } -func (s *composeService) Config(ctx context.Context, project *types.Project, options api.ConfigOptions) ([]byte, error) { - if options.ResolveImageDigests { - var err error - project, err = project.WithImagesResolved(func(named reference.Named) (digest.Digest, error) { - auth, err := encodedAuth(named, s.configFile()) - if err != nil { - return "", err - } - inspect, err := s.apiClient().DistributionInspect(ctx, named.String(), auth) - if err != nil { - return "", err - } - return inspect.Descriptor.Digest, nil - }) - if err != nil { - return nil, err - } - } - - switch options.Format { - case "json": - return project.MarshalJSON() - case "yaml": - return project.MarshalYAML() - default: - return nil, fmt.Errorf("unsupported format %q", options.Format) - } -} - // projectFromName builds a types.Project based on actual resources with compose labels set func (s *composeService) projectFromName(containers Containers, projectName string, services ...string) (*types.Project, error) { project := &types.Project{ diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index 406da5ec2..a01d7be58 100644 --- a/pkg/compose/publish.go +++ b/pkg/compose/publish.go @@ -26,7 +26,6 @@ import ( "github.com/docker/compose/v2/internal/ocipush" "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/progress" - "github.com/opencontainers/go-digest" ) func (s *composeService) Publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error { @@ -111,17 +110,7 @@ func (s *composeService) generateImageDigestsOverride(ctx context.Context, proje if err != nil { return nil, err } - project, err = project.WithImagesResolved(func(named reference.Named) (digest.Digest, error) { - auth, err := encodedAuth(named, s.configFile()) - if err != nil { - return "", err - } - inspect, err := s.apiClient().DistributionInspect(ctx, named.String(), auth) - if err != nil { - return "", err - } - return inspect.Descriptor.Digest, nil - }) + project, err = project.WithImagesResolved(ImageDigestResolver(ctx, s.configFile(), s.apiClient())) if err != nil { return nil, err } diff --git a/pkg/compose/pull.go b/pkg/compose/pull.go index 977397bd0..222876d65 100644 --- a/pkg/compose/pull.go +++ b/pkg/compose/pull.go @@ -28,10 +28,13 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/distribution/reference" "github.com/docker/buildx/driver" + "github.com/docker/cli/cli/config/configfile" moby "github.com/docker/docker/api/types" + "github.com/docker/docker/client" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/registry" "github.com/hashicorp/go-multierror" + "github.com/opencontainers/go-digest" "golang.org/x/sync/errgroup" "github.com/docker/compose/v2/pkg/api" @@ -242,6 +245,21 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser return inspected.ID, nil } +// ImageDigestResolver creates a func able to resolve image digest from a docker ref, +func ImageDigestResolver(ctx context.Context, file *configfile.ConfigFile, apiClient client.APIClient) func(named reference.Named) (digest.Digest, error) { + return func(named reference.Named) (digest.Digest, error) { + auth, err := encodedAuth(named, file) + if err != nil { + return "", err + } + inspect, err := apiClient.DistributionInspect(ctx, named.String(), auth) + if err != nil { + return "", err + } + return inspect.Descriptor.Digest, nil + } +} + func encodedAuth(ref reference.Named, configFile driver.Auth) (string, error) { repoInfo, err := registry.ParseRepositoryInfo(ref) if err != nil { diff --git a/pkg/e2e/compose_test.go b/pkg/e2e/compose_test.go index b37ec0f48..28d8b0eb7 100644 --- a/pkg/e2e/compose_test.go +++ b/pkg/e2e/compose_test.go @@ -235,7 +235,7 @@ func TestCompatibility(t *testing.T) { }) } -func TestConvert(t *testing.T) { +func TestConfig(t *testing.T) { const projectName = "compose-e2e-convert" c := NewParallelCLI(t) @@ -244,20 +244,22 @@ func TestConvert(t *testing.T) { t.Run("up", func(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-build-test/compose.yaml", "-p", projectName, "convert") - res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`services: + res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`name: %s +networks: + default: + name: compose-e2e-convert_default +services: nginx: build: context: %s dockerfile: Dockerfile networks: default: null -networks: - default: - name: compose-e2e-convert_default`, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0}) +`, projectName, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0}) }) } -func TestConvertInterpolate(t *testing.T) { +func TestConfigInterpolate(t *testing.T) { const projectName = "compose-e2e-convert-interpolate" c := NewParallelCLI(t) @@ -266,16 +268,18 @@ func TestConvertInterpolate(t *testing.T) { t.Run("convert", func(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-build-test/compose-interpolate.yaml", "-p", projectName, "convert", "--no-interpolate") - res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`services: + res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`name: %s +networks: + default: + name: compose-e2e-convert-interpolate_default +services: nginx: build: context: %s dockerfile: ${MYVAR} networks: default: null -networks: - default: - name: compose-e2e-convert-interpolate_default`, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0}) +`, projectName, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0}) }) }