Skip to content

Commit

Permalink
move around OCI logic, auto fallback/retry 1.1 -> 1.0
Browse files Browse the repository at this point in the history
Signed-off-by: Milas Bowman <milas.bowman@docker.com>
  • Loading branch information
milas committed Dec 5, 2023
1 parent 596c252 commit b6437d0
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 213 deletions.
3 changes: 3 additions & 0 deletions cmd/compose/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
type publishOptions struct {
*ProjectOptions
resolveImageDigests bool
ociVersion string
}

func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
Expand All @@ -44,6 +45,7 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
}
flags := cmd.Flags()
flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests.")
flags.StringVar(&opts.ociVersion, "oci-version", "", "OCI Image specification version (automatically determined by default)")
return cmd
}

Expand All @@ -55,5 +57,6 @@ func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service,

return backend.Publish(ctx, project, repository, api.PublishOptions{
ResolveImageDigests: opts.resolveImageDigests,
OCIVersion: api.OCIVersion(opts.ociVersion),

Check warning on line 60 in cmd/compose/publish.go

View check run for this annotation

Codecov / codecov/patch

cmd/compose/publish.go#L60

Added line #L60 was not covered by tests
})
}
9 changes: 5 additions & 4 deletions docs/reference/compose_alpha_publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ Publish compose application

### Options

| Name | Type | Default | Description |
|:--------------------------|:-----|:--------|:--------------------------------|
| `--dry-run` | | | Execute command in dry run mode |
| `--resolve-image-digests` | | | Pin image tags to digests. |
| Name | Type | Default | Description |
|:--------------------------|:---------|:--------|:----------------------------------------------------------------------|
| `--dry-run` | | | Execute command in dry run mode |
| `--oci-version` | `string` | | OCI Image specification version (automatically determined by default) |
| `--resolve-image-digests` | | | Pin image tags to digests. |


<!---MARKER_GEN_END-->
Expand Down
10 changes: 10 additions & 0 deletions docs/reference/docker_compose_alpha_publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ usage: docker compose alpha publish [OPTIONS] [REPOSITORY]
pname: docker compose alpha
plink: docker_compose_alpha.yaml
options:
- option: oci-version
value_type: string
description: |
OCI Image specification version (automatically determined by default)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: resolve-image-digests
value_type: bool
default_value: "false"
Expand Down
183 changes: 183 additions & 0 deletions internal/ocipush/push.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
Copyright 2023 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 ocipush

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"path/filepath"
"time"

pusherrors "github.com/containerd/containerd/remotes/errors"
"github.com/distribution/reference"
"github.com/docker/buildx/util/imagetools"
"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"
)

// clientAuthStatusCodes are client (4xx) errors that are authentication
// related.
var clientAuthStatusCodes = []int{
http.StatusUnauthorized,
http.StatusForbidden,
http.StatusProxyAuthRequired,
}

type Pushable struct {
Descriptor v1.Descriptor
Data []byte
}

func DescriptorForComposeFile(path string, content []byte) v1.Descriptor {
return v1.Descriptor{
MediaType: "application/vnd.docker.compose.file+yaml",
Digest: digest.FromString(string(content)),
Size: int64(len(content)),
Annotations: map[string]string{
"com.docker.compose.version": api.ComposeVersion,
"com.docker.compose.file": filepath.Base(path),
},
}

Check warning on line 59 in internal/ocipush/push.go

View check run for this annotation

Codecov / codecov/patch

internal/ocipush/push.go#L50-L59

Added lines #L50 - L59 were not covered by tests
}

func PushManifest(
ctx context.Context,
resolver *imagetools.Resolver,
named reference.Named,
layers []Pushable,
ociVersion api.OCIVersion,
) error {
// prepare to push the manifest by pushing the layers
layerDescriptors := make([]v1.Descriptor, len(layers))
for i := range layers {
layerDescriptors[i] = layers[i].Descriptor
if err := resolver.Push(ctx, named, layers[i].Descriptor, layers[i].Data); err != nil {
return err
}

Check warning on line 75 in internal/ocipush/push.go

View check run for this annotation

Codecov / codecov/patch

internal/ocipush/push.go#L68-L75

Added lines #L68 - L75 were not covered by tests
}

if ociVersion != "" {
// if a version was explicitly specified, use it
return createAndPushManifest(ctx, resolver, named, layerDescriptors, ociVersion)
}

Check warning on line 81 in internal/ocipush/push.go

View check run for this annotation

Codecov / codecov/patch

internal/ocipush/push.go#L78-L81

Added lines #L78 - L81 were not covered by tests

// try to push in the OCI 1.1 format but fallback to OCI 1.0 on 4xx errors
// (other than auth) since it's most likely the result of the registry not
// having support
err := createAndPushManifest(ctx, resolver, named, layerDescriptors, api.OCIVersion1_1)
var pushErr pusherrors.ErrUnexpectedStatus
if errors.As(err, &pushErr) && isNonAuthClientError(pushErr.StatusCode) {
// TODO(milas): show a warning here (won't work with logrus)
return createAndPushManifest(ctx, resolver, named, layerDescriptors, api.OCIVersion1_0)
}
return err

Check warning on line 92 in internal/ocipush/push.go

View check run for this annotation

Codecov / codecov/patch

internal/ocipush/push.go#L86-L92

Added lines #L86 - L92 were not covered by tests
}

func createAndPushManifest(
ctx context.Context,
resolver *imagetools.Resolver,
named reference.Named,
layers []v1.Descriptor,
ociVersion api.OCIVersion,
) error {
toPush, err := generateManifest(layers, ociVersion)
if err != nil {
return err
}
for _, p := range toPush {
err = resolver.Push(ctx, named, p.Descriptor, p.Data)
if err != nil {
return err
}

Check warning on line 110 in internal/ocipush/push.go

View check run for this annotation

Codecov / codecov/patch

internal/ocipush/push.go#L101-L110

Added lines #L101 - L110 were not covered by tests
}
return nil

Check warning on line 112 in internal/ocipush/push.go

View check run for this annotation

Codecov / codecov/patch

internal/ocipush/push.go#L112

Added line #L112 was not covered by tests
}

func isNonAuthClientError(statusCode int) bool {
if statusCode < 400 || statusCode >= 500 {
// not a client error
return false
}
for _, v := range clientAuthStatusCodes {
if statusCode == v {
// client auth error
return false
}

Check warning on line 124 in internal/ocipush/push.go

View check run for this annotation

Codecov / codecov/patch

internal/ocipush/push.go#L115-L124

Added lines #L115 - L124 were not covered by tests
}
// any other 4xx client error
return true

Check warning on line 127 in internal/ocipush/push.go

View check run for this annotation

Codecov / codecov/patch

internal/ocipush/push.go#L127

Added line #L127 was not covered by tests
}

func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) ([]Pushable, error) {
var toPush []Pushable
var config v1.Descriptor
var artifactType string
switch ociCompat {
case api.OCIVersion1_0:
configData, err := json.Marshal(v1.ImageConfig{})
if err != nil {
return nil, err
}
config = v1.Descriptor{
MediaType: v1.MediaTypeImageConfig,
Digest: digest.FromBytes(configData),
Size: int64(len(configData)),
}
// N.B. OCI 1.0 does NOT support specifying the artifact type, so it's
// left as an empty string to omit it from the marshaled JSON
artifactType = ""
toPush = append(toPush, Pushable{Descriptor: config, Data: configData})
case api.OCIVersion1_1:
config = v1.DescriptorEmptyJSON
artifactType = "application/vnd.docker.compose.project"
// N.B. the descriptor has the data embedded in it
toPush = append(toPush, Pushable{Descriptor: config, Data: nil})
default:
return nil, fmt.Errorf("unsupported OCI version: %s", ociCompat)

Check warning on line 155 in internal/ocipush/push.go

View check run for this annotation

Codecov / codecov/patch

internal/ocipush/push.go#L130-L155

Added lines #L130 - L155 were not covered by tests
}

manifest, err := json.Marshal(v1.Manifest{
Versioned: specs.Versioned{SchemaVersion: 2},
MediaType: v1.MediaTypeImageManifest,
ArtifactType: artifactType,
Config: config,
Layers: layers,
Annotations: map[string]string{
"org.opencontainers.image.created": time.Now().Format(time.RFC3339),
},
})
if err != nil {
return nil, err
}

Check warning on line 170 in internal/ocipush/push.go

View check run for this annotation

Codecov / codecov/patch

internal/ocipush/push.go#L158-L170

Added lines #L158 - L170 were not covered by tests

manifestDescriptor := v1.Descriptor{
MediaType: v1.MediaTypeImageManifest,
Digest: digest.FromString(string(manifest)),
Size: int64(len(manifest)),
Annotations: map[string]string{
"com.docker.compose.version": api.ComposeVersion,
},
ArtifactType: artifactType,
}
toPush = append(toPush, Pushable{Descriptor: manifestDescriptor, Data: manifest})
return toPush, nil

Check warning on line 182 in internal/ocipush/push.go

View check run for this annotation

Codecov / codecov/patch

internal/ocipush/push.go#L172-L182

Added lines #L172 - L182 were not covered by tests
}
19 changes: 19 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,28 @@ type PortOptions struct {
Index int
}

// OCIVersion controls manifest generation to ensure compatibility
// with different registries.
//
// Currently, this is not exposed as an option to the user – Compose uses
// OCI 1.0 mode automatically for ECR registries based on domain and OCI 1.1
// for all other registries.
//
// There are likely other popular registries that do not support the OCI 1.1
// format, so it might make sense to expose this as a CLI flag or see if
// there's a way to generically probe the registry for support level.
type OCIVersion string

const (
OCIVersion1_0 OCIVersion = "1.0"
OCIVersion1_1 OCIVersion = "1.1"
)

// PublishOptions group options of the Publish API
type PublishOptions struct {
ResolveImageDigests bool

OCIVersion OCIVersion
}

func (e Event) String() string {
Expand Down
Loading

0 comments on commit b6437d0

Please sign in to comment.