Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 43 additions & 29 deletions internal/ocipush/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ import (
"slices"
"time"

"github.com/containerd/containerd/v2/core/remotes"
pusherrors "github.com/containerd/containerd/v2/core/remotes/errors"
"github.com/containerd/errdefs"
"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"
Expand Down Expand Up @@ -67,11 +68,6 @@ var clientAuthStatusCodes = []int{
http.StatusProxyAuthRequired,
}

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

func DescriptorForComposeFile(path string, content []byte) v1.Descriptor {
return v1.Descriptor{
MediaType: ComposeYAMLMediaType,
Expand All @@ -81,6 +77,7 @@ func DescriptorForComposeFile(path string, content []byte) v1.Descriptor {
"com.docker.compose.version": api.ComposeVersion,
"com.docker.compose.file": filepath.Base(path),
},
Data: content,
}
}

Expand All @@ -93,27 +90,23 @@ func DescriptorForEnvFile(path string, content []byte) v1.Descriptor {
"com.docker.compose.version": api.ComposeVersion,
"com.docker.compose.envfile": filepath.Base(path),
},
Data: content,
}
}

func PushManifest(
ctx context.Context,
resolver *imagetools.Resolver,
named reference.Named,
layers []Pushable,
ociVersion api.OCIVersion,
) error {
func PushManifest(ctx context.Context, resolver remotes.Resolver, named reference.Named, layers []v1.Descriptor, ociVersion api.OCIVersion) error {
// Check if we need an extra empty layer for the manifest config
if ociVersion == api.OCIVersion1_1 || ociVersion == "" {
if err := resolver.Push(ctx, named, v1.DescriptorEmptyJSON, v1.DescriptorEmptyJSON.Data); err != nil {
err := push(ctx, resolver, named, v1.DescriptorEmptyJSON)
if err != nil {
return err
}
}
// 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 {
layerDescriptors[i] = layers[i]
if err := push(ctx, resolver, named, layers[i]); err != nil {
return err
}
}
Expand All @@ -135,19 +128,38 @@ func PushManifest(
return err
}

func createAndPushManifest(
ctx context.Context,
resolver *imagetools.Resolver,
named reference.Named,
layers []v1.Descriptor,
ociVersion api.OCIVersion,
) error {
func push(ctx context.Context, resolver remotes.Resolver, ref reference.Named, descriptor v1.Descriptor) error {
fullRef, err := reference.WithDigest(reference.TagNameOnly(ref), descriptor.Digest)
if err != nil {
return err
}

pusher, err := resolver.Pusher(ctx, fullRef.String())
if err != nil {
return err
}
push, err := pusher.Push(ctx, descriptor)
if errdefs.IsAlreadyExists(err) {
return nil
}
if err != nil {
return err
}
defer func() {
_ = push.Close()
}()

_, err = push.Write(descriptor.Data)
return err
}

func createAndPushManifest(ctx context.Context, resolver remotes.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)
err = push(ctx, resolver, named, p)
if err != nil {
return err
}
Expand All @@ -163,8 +175,8 @@ func isNonAuthClientError(statusCode int) bool {
return !slices.Contains(clientAuthStatusCodes, statusCode)
}

func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) ([]Pushable, error) {
var toPush []Pushable
func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) ([]v1.Descriptor, error) {
var toPush []v1.Descriptor
var config v1.Descriptor
var artifactType string
switch ociCompat {
Expand All @@ -184,16 +196,17 @@ func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) ([]Pusha
MediaType: ComposeEmptyConfigMediaType,
Digest: digest.FromBytes(configData),
Size: int64(len(configData)),
Data: 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})
toPush = append(toPush, config)
case api.OCIVersion1_1:
config = v1.DescriptorEmptyJSON
artifactType = ComposeProjectArtifactType
// N.B. the descriptor has the data embedded in it
toPush = append(toPush, Pushable{Descriptor: config, Data: make([]byte, len(config.Data))})
toPush = append(toPush, config)
default:
return nil, fmt.Errorf("unsupported OCI version: %s", ociCompat)
}
Expand All @@ -220,7 +233,8 @@ func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) ([]Pusha
"com.docker.compose.version": api.ComposeVersion,
},
ArtifactType: artifactType,
Data: manifest,
}
toPush = append(toPush, Pushable{Descriptor: manifestDescriptor, Data: manifest})
toPush = append(toPush, manifestDescriptor)
return toPush, nil
}
18 changes: 12 additions & 6 deletions internal/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,28 @@

package registry

import "github.com/distribution/reference"

const (
// DefaultNamespace is the default namespace
DefaultNamespace = "docker.io"
// DefaultRegistryHost is the hostname for the default (Docker Hub) registry
// used for pushing and pulling images. This hostname is hard-coded to handle
// the conversion from image references without registry name (e.g. "ubuntu",
// or "ubuntu:latest"), as well as references using the "docker.io" domain
// name, which is used as canonical reference for images on Docker Hub, but
// does not match the domain-name of Docker Hub's registry.
DefaultRegistryHost = "registry-1.docker.io"
// IndexHostname is the index hostname, used for authentication and image search.
IndexHostname = "index.docker.io"
// IndexServer is used for user auth and image search
IndexServer = "https://index.docker.io/v1/"
IndexServer = "https://" + IndexHostname + "/v1/"
// IndexName is the name of the index
IndexName = "docker.io"
)

// GetAuthConfigKey special-cases using the full index address of the official
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
func GetAuthConfigKey(reposName reference.Named) string {
indexName := reference.Domain(reposName)
if indexName == IndexName || indexName == IndexHostname {
func GetAuthConfigKey(indexName string) string {
if indexName == IndexName || indexName == IndexHostname || indexName == DefaultRegistryHost {
return IndexServer
}
return indexName
Expand Down
55 changes: 30 additions & 25 deletions pkg/compose/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,18 @@ import (

"github.com/DefangLabs/secret-detector/pkg/scanner"
"github.com/DefangLabs/secret-detector/pkg/secrets"

"github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/containerd/v2/core/remotes/docker"
"github.com/distribution/reference"
"github.com/docker/buildx/util/imagetools"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/internal/ocipush"
"github.com/docker/compose/v2/internal/registry"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose/transform"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/prompt"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)

func (s *composeService) Publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error {
Expand All @@ -64,11 +65,27 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
return err
}

resolver := imagetools.New(imagetools.Opt{
Auth: s.configFile(),
config := s.dockerCli.ConfigFile()

resolver := docker.NewResolver(docker.ResolverOptions{
Hosts: docker.ConfigureDefaultRegistries(
docker.WithAuthorizer(docker.NewDockerAuthorizer(
docker.WithAuthCreds(func(host string) (string, string, error) {
host = registry.GetAuthConfigKey(host)
auth, err := config.GetAuthConfig(host)
if err != nil {
return "", "", err
}
if auth.IdentityToken != "" {
return "", auth.IdentityToken, nil
}
return auth.Username, auth.Password, nil
}),
)),
),
})

var layers []ocipush.Pushable
var layers []v1.Descriptor
extFiles := map[string]string{}
for _, file := range project.ComposeFiles {
data, err := processFile(ctx, file, project, extFiles)
Expand All @@ -77,10 +94,7 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
}

layerDescriptor := ocipush.DescriptorForComposeFile(file, data)
layers = append(layers, ocipush.Pushable{
Descriptor: layerDescriptor,
Data: data,
})
layers = append(layers, layerDescriptor)
}

extLayers, err := processExtends(ctx, project, extFiles)
Expand All @@ -100,10 +114,7 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
}

layerDescriptor := ocipush.DescriptorForComposeFile("image-digests.yaml", yaml)
layers = append(layers, ocipush.Pushable{
Descriptor: layerDescriptor,
Data: yaml,
})
layers = append(layers, layerDescriptor)
}

w := progress.ContextWriter(ctx)
Expand Down Expand Up @@ -131,8 +142,8 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
return nil
}

func processExtends(ctx context.Context, project *types.Project, extFiles map[string]string) ([]ocipush.Pushable, error) {
var layers []ocipush.Pushable
func processExtends(ctx context.Context, project *types.Project, extFiles map[string]string) ([]v1.Descriptor, error) {
var layers []v1.Descriptor
moreExtFiles := map[string]string{}
for xf, hash := range extFiles {
data, err := processFile(ctx, xf, project, moreExtFiles)
Expand All @@ -142,10 +153,7 @@ func processExtends(ctx context.Context, project *types.Project, extFiles map[st

layerDescriptor := ocipush.DescriptorForComposeFile(hash, data)
layerDescriptor.Annotations["com.docker.compose.extends"] = "true"
layers = append(layers, ocipush.Pushable{
Descriptor: layerDescriptor,
Data: data,
})
layers = append(layers, layerDescriptor)
}
for f, hash := range moreExtFiles {
if _, ok := extFiles[f]; ok {
Expand Down Expand Up @@ -343,8 +351,8 @@ func acceptPublishBindMountDeclarations(cli command.Cli) (bool, error) {
return confirm, err
}

func envFileLayers(project *types.Project) []ocipush.Pushable {
var layers []ocipush.Pushable
func envFileLayers(project *types.Project) []v1.Descriptor {
var layers []v1.Descriptor
for _, service := range project.Services {
for _, envFile := range service.EnvFiles {
f, err := os.ReadFile(envFile.Path)
Expand All @@ -353,10 +361,7 @@ func envFileLayers(project *types.Project) []ocipush.Pushable {
continue
}
layerDescriptor := ocipush.DescriptorForEnvFile(envFile.Path, f)
layers = append(layers, ocipush.Pushable{
Descriptor: layerDescriptor,
Data: f,
})
layers = append(layers, layerDescriptor)
}
}
return layers
Expand Down
19 changes: 8 additions & 11 deletions pkg/compose/publish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (

"github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/internal/ocipush"
"github.com/docker/compose/v2/pkg/api"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(very unrelated) In our other repositories, we alias this import as either ocispec (or ocispecs); I can open a follow-up PR to update the aliases in this repo, which may help reading the code.

"gotest.tools/v3/assert"
Expand Down Expand Up @@ -58,17 +57,15 @@ services:

b, err := os.ReadFile("testdata/publish/common.yaml")
assert.NilError(t, err)
assert.DeepEqual(t, []ocipush.Pushable{
assert.DeepEqual(t, []v1.Descriptor{
{
Descriptor: v1.Descriptor{
MediaType: "application/vnd.docker.compose.file+yaml",
Digest: "sha256:d3ba84507b56ec783f4b6d24306b99a15285f0a23a835f0b668c2dbf9c59c241",
Size: 32,
Annotations: map[string]string{
"com.docker.compose.extends": "true",
"com.docker.compose.file": "f8f9ede3d201ec37d5a5e3a77bbadab79af26035e53135e19571f50d541d390c.yaml",
"com.docker.compose.version": api.ComposeVersion,
},
MediaType: "application/vnd.docker.compose.file+yaml",
Digest: "sha256:d3ba84507b56ec783f4b6d24306b99a15285f0a23a835f0b668c2dbf9c59c241",
Size: 32,
Annotations: map[string]string{
"com.docker.compose.extends": "true",
"com.docker.compose.file": "f8f9ede3d201ec37d5a5e3a77bbadab79af26035e53135e19571f50d541d390c.yaml",
"com.docker.compose.version": api.ComposeVersion,
},
Data: b,
},
Expand Down
2 changes: 1 addition & 1 deletion pkg/compose/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ func ImageDigestResolver(ctx context.Context, file *configfile.ConfigFile, apiCl
}

func encodedAuth(ref reference.Named, configFile driver.Auth) (string, error) {
authConfig, err := configFile.GetAuthConfig(registry.GetAuthConfigKey(ref))
authConfig, err := configFile.GetAuthConfig(registry.GetAuthConfigKey(reference.Domain(ref)))
if err != nil {
return "", err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/compose/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func (s *composeService) pushServiceImage(ctx context.Context, tag string, confi
return err
}

authConfig, err := configFile.GetAuthConfig(registry.GetAuthConfigKey(ref))
authConfig, err := configFile.GetAuthConfig(registry.GetAuthConfigKey(reference.Domain(ref)))
if err != nil {
return err
}
Expand Down