use containerd registry client

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof
2025-09-25 14:25:39 +02:00
committed by Nicolas De loof
parent 032e0309ee
commit 8978c1027d
6 changed files with 95 additions and 73 deletions

View File

@@ -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"
@@ -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,
@@ -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,
}
}
@@ -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
}
}
@@ -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
}
@@ -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 {
@@ -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)
}
@@ -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
}

View File

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

View File

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

View File

@@ -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"
"gotest.tools/v3/assert"
@@ -58,9 +57,8 @@ 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,
@@ -69,7 +67,6 @@ services:
"com.docker.compose.file": "f8f9ede3d201ec37d5a5e3a77bbadab79af26035e53135e19571f50d541d390c.yaml",
"com.docker.compose.version": api.ComposeVersion,
},
},
Data: b,
},
}, layers)

View File

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

View File

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