Compare commits

...

2 Commits

Author SHA1 Message Date
Nicolas De Loof
f06caeb844 oras doesn't prepend index.docker.io to repository ref
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-07-07 09:26:13 +02:00
Nicolas De Loof
49d1bc7524 introduce push --repository
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-07-05 09:06:41 +02:00
9 changed files with 328 additions and 7 deletions

View File

@@ -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
cmd/compose/publish.go Normal file
View File

@@ -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)
}

View File

@@ -31,6 +31,7 @@ type pushOptions struct {
IncludeDeps bool
Ignorefailures bool
Quiet bool
Repository string
}
func pushCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
@@ -48,6 +49,7 @@ func pushCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
pushCmd.Flags().BoolVar(&opts.Ignorefailures, "ignore-push-failures", false, "Push what it can and ignores images with push failures")
pushCmd.Flags().BoolVar(&opts.IncludeDeps, "include-deps", false, "Also push images of services declared as dependencies")
pushCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Push without printing progress information")
pushCmd.Flags().StringVarP(&opts.Repository, "repository", "r", "", "Also publish the compose application in repository")
return pushCmd
}
@@ -68,5 +70,6 @@ func runPush(ctx context.Context, backend api.Service, opts pushOptions, service
return backend.Push(ctx, project, api.PushOptions{
IgnoreFailures: opts.Ignorefailures,
Quiet: opts.Quiet,
Repository: opts.Repository,
})
}

3
go.mod
View File

@@ -29,7 +29,7 @@ require (
github.com/moby/term v0.5.0
github.com/morikuni/aec v1.0.0
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0-rc3
github.com/opencontainers/image-spec v1.1.0-rc4
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
@@ -183,6 +183,7 @@ require (
k8s.io/klog/v2 v2.90.1 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect
oras.land/oras-go/v2 v2.2.0 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect

4
go.sum
View File

@@ -513,6 +513,8 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8=
github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0=
github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
github.com/opencontainers/runc v1.1.7 h1:y2EZDS8sNng4Ksf0GUYNhKbTShZJPJg1FiXJNH/uoCk=
github.com/opencontainers/runc v1.1.7/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50=
github.com/opencontainers/runtime-spec v1.1.0-rc.2 h1:ucBtEms2tamYYW/SvGpvq9yUN0NEVL6oyLEwDcTSrk8=
@@ -1110,6 +1112,8 @@ k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+O
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4=
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 h1:kmDqav+P+/5e1i9tFfHq1qcF3sOrDp+YEkVDAHu7Jwk=
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go/v2 v2.2.0 h1:E1fqITD56Eg5neZbxBtAdZVgDHD6wBabJo6xESTcQyo=
oras.land/oras-go/v2 v2.2.0/go.mod h1:pXjn0+KfarspMHHNR3A56j3tgvr+mxArHuI8qVn59v8=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -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
@@ -260,6 +262,7 @@ type ConfigOptions struct {
// PushOptions group options of the Push API
type PushOptions struct {
Quiet bool
Repository string
IgnoreFailures bool
}

View File

@@ -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
pkg/compose/publish.go Normal file
View File

@@ -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
}

View File

@@ -17,6 +17,7 @@
package compose
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
@@ -26,14 +27,17 @@ import (
"github.com/compose-spec/compose-go/types"
"github.com/distribution/distribution/v3/reference"
"github.com/docker/buildx/driver"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
moby "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/registry"
"github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/registry/remote"
)
func (s *composeService) Push(ctx context.Context, project *types.Project, options api.PushOptions) error {
@@ -45,8 +49,8 @@ func (s *composeService) Push(ctx context.Context, project *types.Project, optio
}, s.stdinfo(), "Pushing")
}
func (s *composeService) push(ctx context.Context, project *types.Project, options api.PushOptions) error {
eg, ctx := errgroup.WithContext(ctx)
func (s *composeService) push(upctx context.Context, project *types.Project, options api.PushOptions) error {
eg, ctx := errgroup.WithContext(upctx)
eg.SetLimit(s.maxConcurrency)
info, err := s.apiClient().Info(ctx)
@@ -79,7 +83,66 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
return nil
})
}
return eg.Wait()
err = eg.Wait()
if err != nil {
return err
}
ctx = upctx
if options.Repository != "" {
repository, err := remote.NewRepository(options.Repository)
if err != nil {
return err
}
yaml, err := project.MarshalYAML()
if err != nil {
return err
}
manifests := []v1.Descriptor{
{
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
Digest: digest.FromBytes(yaml),
Size: int64(len(yaml)),
Data: yaml,
ArtifactType: "application/vnd.docker.compose.yaml",
},
}
for _, service := range project.Services {
inspected, _, err := s.dockerCli.Client().ImageInspectWithRaw(ctx, service.Image)
if err != nil {
return err
}
manifests = append(manifests, v1.Descriptor{
MediaType: v1.MediaTypeImageIndex,
Digest: digest.Digest(inspected.RepoDigests[0]),
Size: inspected.Size,
Annotations: map[string]string{
"com.docker.compose.service": service.Name,
},
})
}
manifest := v1.Index{
MediaType: v1.MediaTypeImageIndex,
Manifests: manifests,
Annotations: map[string]string{
"com.docker.compose": api.ComposeVersion,
},
}
manifestContent, err := json.Marshal(manifest)
if err != nil {
return err
}
manifestDescriptor := content.NewDescriptorFromBytes(v1.MediaTypeImageIndex, manifestContent)
err = repository.Push(ctx, manifestDescriptor, bytes.NewReader(manifestContent))
if err != nil {
return err
}
}
return nil
}
func (s *composeService) pushServiceImage(ctx context.Context, service types.ServiceConfig, info moby.Info, configFile driver.Auth, w progress.Writer, quietPush bool) error {