From f06caeb844473dda9d625b2b77335b56e4d826b8 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 5 Jul 2023 09:08:03 +0200 Subject: [PATCH] oras doesn't prepend index.docker.io to repository ref Signed-off-by: Nicolas De Loof --- cmd/compose/compose.go | 1 + cmd/compose/publish.go | 55 ++++++++++++ pkg/api/api.go | 2 + pkg/api/proxy.go | 6 ++ pkg/compose/publish.go | 185 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 249 insertions(+) create mode 100644 cmd/compose/publish.go create mode 100644 pkg/compose/publish.go diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 04f4ea88d..b8ca5401b 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -427,6 +427,7 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no imagesCommand(&opts, streams, backend), versionCommand(streams), buildCommand(&opts, &progress, backend), + publishCommand(&opts, backend), pushCommand(&opts, backend), pullCommand(&opts, backend), createCommand(&opts, backend), diff --git a/cmd/compose/publish.go b/cmd/compose/publish.go new file mode 100644 index 000000000..9369afefc --- /dev/null +++ b/cmd/compose/publish.go @@ -0,0 +1,55 @@ +/* + 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" + + "github.com/spf13/cobra" + + "github.com/docker/compose/v2/pkg/api" +) + +type publishOptions struct { + *ProjectOptions + composeOptions + Repository string +} + +func publishCommand(p *ProjectOptions, backend api.Service) *cobra.Command { + opts := pushOptions{ + ProjectOptions: p, + } + publishCmd := &cobra.Command{ + Use: "publish [OPTIONS] [REPOSITORY]", + Short: "Publish compose application", + RunE: Adapt(func(ctx context.Context, args []string) error { + return runPublish(ctx, backend, opts, args[0]) + }), + Args: cobra.ExactArgs(1), + } + return publishCmd +} + +func runPublish(ctx context.Context, backend api.Service, opts pushOptions, repository string) error { + project, err := opts.ToProject(nil) + if err != nil { + return err + } + + return backend.Publish(ctx, project, repository) +} diff --git a/pkg/api/api.go b/pkg/api/api.go index a916822b0..8949c560f 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -74,6 +74,8 @@ type Service interface { Events(ctx context.Context, projectName string, options EventsOptions) error // Port executes the equivalent to a `compose port` Port(ctx context.Context, projectName string, service string, port uint16, options PortOptions) (string, int, error) + // Publish executes the equivalent to a `compose publish` + Publish(ctx context.Context, project *types.Project, repository string) error // Images executes the equivalent of a `compose images` Images(ctx context.Context, projectName string, options ImagesOptions) ([]ImageSummary, error) // MaxConcurrency defines upper limit for concurrent operations against engine API diff --git a/pkg/api/proxy.go b/pkg/api/proxy.go index aeb773c30..4db414353 100644 --- a/pkg/api/proxy.go +++ b/pkg/api/proxy.go @@ -55,6 +55,7 @@ type ServiceProxy struct { DryRunModeFn func(ctx context.Context, dryRun bool) (context.Context, error) VizFn func(ctx context.Context, project *types.Project, options VizOptions) (string, error) WaitFn func(ctx context.Context, projectName string, options WaitOptions) (int64, error) + PublishFn func(ctx context.Context, project *types.Project, repository string) error interceptors []Interceptor } @@ -91,6 +92,7 @@ func (s *ServiceProxy) WithService(service Service) *ServiceProxy { s.TopFn = service.Top s.EventsFn = service.Events s.PortFn = service.Port + s.PublishFn = service.Publish s.ImagesFn = service.Images s.WatchFn = service.Watch s.MaxConcurrencyFn = service.MaxConcurrency @@ -311,6 +313,10 @@ func (s *ServiceProxy) Port(ctx context.Context, projectName string, service str return s.PortFn(ctx, projectName, service, port, options) } +func (s *ServiceProxy) Publish(ctx context.Context, project *types.Project, repository string) error { + return s.PublishFn(ctx, project, repository) +} + // Images implements Service interface func (s *ServiceProxy) Images(ctx context.Context, project string, options ImagesOptions) ([]ImageSummary, error) { if s.ImagesFn == nil { diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go new file mode 100644 index 000000000..919f6fe93 --- /dev/null +++ b/pkg/compose/publish.go @@ -0,0 +1,185 @@ +/* + 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" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "strings" + + "github.com/compose-spec/compose-go/types" + "github.com/distribution/distribution/v3/reference" + client2 "github.com/docker/cli/cli/registry/client" + "github.com/docker/compose/v2/pkg/api" + "github.com/opencontainers/go-digest" + "github.com/opencontainers/image-spec/specs-go" + v1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +func (s *composeService) Publish(ctx context.Context, project *types.Project, repository string) error { + err := s.Push(ctx, project, api.PushOptions{}) + if err != nil { + return err + } + + target, err := reference.ParseDockerRef(repository) + if err != nil { + return err + } + client := s.dockerCli.RegistryClient(false) + for i, service := range project.Services { + ref, err := reference.ParseDockerRef(service.Image) + if err != nil { + return err + } + auth, err := encodedAuth(ref, s.configFile()) + if err != nil { + return err + } + inspect, err := s.apiClient().DistributionInspect(ctx, ref.String(), auth) + if err != nil { + return err + } + canonical, err := reference.WithDigest(ref, inspect.Descriptor.Digest) + if err != nil { + return err + } + to, err := reference.WithDigest(target, inspect.Descriptor.Digest) + if err != nil { + return err + } + err = client.MountBlob(ctx, canonical, to) + switch err.(type) { + case client2.ErrBlobCreated: + default: + return err + } + service.Image = to.String() + project.Services[i] = service + } + + err = s.publishComposeYaml(ctx, project, repository) + if err != nil { + return err + } + return nil +} + +func (s *composeService) publishComposeYaml(ctx context.Context, project *types.Project, repository string) error { + ref, err := reference.ParseDockerRef(repository) + if err != nil { + return err + } + + var manifests []v1.Descriptor + + for _, composeFile := range project.ComposeFiles { + stat, err := os.Stat(composeFile) + if err != nil { + return err + } + + cmd := exec.CommandContext(ctx, "oras", "push", "--artifact-type", "application/vnd.docker.compose.yaml", ref.String(), composeFile) + stdout, err := cmd.StdoutPipe() + if err != nil { + return err + } + cmd.Stderr = s.stderr() + + err = cmd.Start() + if err != nil { + return err + } + out, err := io.ReadAll(stdout) + if err != nil { + return err + } + var composeFileDigest string + for _, line := range strings.Split(string(out), "\n") { + if strings.HasPrefix(line, "Digest: ") { + composeFileDigest = line[len("Digest: "):] + } + fmt.Fprintln(s.stdout(), line) + } + if composeFileDigest == "" { + return fmt.Errorf("expected oras to display `Digest: xxx`") + } + + err = cmd.Wait() + if err != nil { + return err + } + + manifests = append(manifests, v1.Descriptor{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + Digest: digest.Digest(composeFileDigest), + Size: stat.Size(), + ArtifactType: "application/vnd.docker.compose.yaml", + }) + } + + for _, service := range project.Services { + dockerRef, err := reference.ParseDockerRef(service.Image) + if err != nil { + return err + } + manifests = append(manifests, v1.Descriptor{ + MediaType: v1.MediaTypeImageIndex, + Digest: dockerRef.(reference.Digested).Digest(), + Annotations: map[string]string{ + "com.docker.compose.service": service.Name, + }, + }) + } + + manifest := v1.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + MediaType: v1.MediaTypeImageIndex, + Manifests: manifests, + Annotations: map[string]string{ + "com.docker.compose": api.ComposeVersion, + }, + } + manifestContent, err := json.Marshal(manifest) + if err != nil { + return err + } + temp, err := os.CreateTemp(os.TempDir(), "compose") + if err != nil { + return err + } + err = os.WriteFile(temp.Name(), manifestContent, 0o700) + if err != nil { + return err + } + defer os.Remove(temp.Name()) + + cmd := exec.CommandContext(ctx, "oras", "manifest", "push", ref.String(), temp.Name()) + cmd.Stdout = s.stdout() + cmd.Stderr = s.stderr() + err = cmd.Run() + if err != nil { + return err + } + return nil +}