Browse Source

oras doesn't prepend index.docker.io to repository ref

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof 2 years ago
parent
commit
f06caeb844
5 changed files with 249 additions and 0 deletions
  1. 1 0
      cmd/compose/compose.go
  2. 55 0
      cmd/compose/publish.go
  3. 2 0
      pkg/api/api.go
  4. 6 0
      pkg/api/proxy.go
  5. 185 0
      pkg/compose/publish.go

+ 1 - 0
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),

+ 55 - 0
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)
+}

+ 2 - 0
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

+ 6 - 0
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 {

+ 185 - 0
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
+}