registry

package module
v0.0.0-...-9a39f97 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 8, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

README ΒΆ

πŸ“¦ registry

A comprehensive container registry client package for Golang applications that provides an intuitive interface for interacting with OCI-compliant container registries. Built on top of google/go-containerregistry, this package offers enhanced functionality with path segmentation, flexible authentication, and seamless integration with the Deckhouse ecosystem.

Table of Contents

Overview

The registry package provides a high-level, production-ready client for container registry operations. It abstracts the complexity of working with OCI registries while providing powerful features for building repository paths, managing authentication, and performing common registry operations.

Key capabilities:

  • Fluent Path Building: Chain WithSegment() calls to construct complex repository paths
  • Flexible Authentication: Support for various authentication methods via authn.Authenticator, keychains, and Docker config JSON
  • Rich Image Operations: Pull, push, inspect, extract, retag, copy, and delete container images
  • Multi-Arch Support: Push and pull image indexes (multi-platform manifest lists)
  • Repository Management: List tags and enumerate repositories with server-side pagination
  • Transport Middlewares: Composable middleware chain for metrics, tracing, logging, and rate-limiting
  • Thread-Safe: All operations are safe for concurrent use
  • Context-Aware: Full support for context cancellation and timeouts

Features

  • Container Image Management

    • Pull images with tag or digest references
    • Push single images and multi-arch image indexes
    • Extract flattened image content
    • Retrieve image configurations and metadata
    • Platform-specific image retrieval for multi-arch images
  • Tag & Lifecycle Operations

    • Retag images without re-uploading layers (efficient manifest PUT)
    • Delete tags from registries
    • Delete manifests by digest
    • Copy images between registries (server-side mount when possible)
  • Registry Operations

    • List all tags in a repository with server-side pagination
    • Enumerate sub-repositories with server-side pagination
    • Check image existence (HEAD with GET fallback)
    • Get image digests and manifests
  • Flexible Configuration

    • Authentication via authn.Authenticator, authn.Keychain, or Docker config JSON
    • TLS configuration (custom CA, skip verification, insecure HTTP)
    • Custom HTTP transports and explicit proxy support
    • Transport middleware chain (metrics, tracing, logging, rate-limiting)
    • Structured logging integration
    • Per-operation timeouts
  • Developer-Friendly

    • Chainable API for building repository paths
    • Clean interface/implementation separation (registry interfaces, client implementation)
    • Type-safe option interfaces with apply pattern
    • Comprehensive error types and sentinel errors
    • Context support for all operations

Installation

go get github.com/deckhouse/deckhouse/pkg/registry

Quick Start

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/google/go-containerregistry/pkg/authn"

    decklog "github.com/deckhouse/deckhouse/pkg/log"
    "github.com/deckhouse/deckhouse/pkg/registry/client"
)

func main() {
    ctx := context.Background()
    logger := decklog.NewLogger().Named("registry")

    // Create base client using functional options (preferred)
    registryClient := client.New("registry.example.com",
        client.WithLoginPassword("myuser", "mypassword"),
        client.WithLogger(logger),
    )

    // Or using the Options struct directly
    auth := authn.FromConfig(authn.AuthConfig{
        Username: "myuser",
        Password: "mypassword",
    })
    opts := &client.Options{
        Auth:   auth,
        Logger: logger,
    }
    registryClient = client.NewClientWithOptions("registry.example.com", opts)

    // Build repository path using segments
    moduleClient := registryClient.
        WithSegment("deckhouse").
        WithSegment("modules").
        WithSegment("my-module")

    // List available tags
    tags, err := moduleClient.ListTags(ctx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Available tags: %v\n", tags)

    // Pull and inspect an image
    img, err := moduleClient.GetImage(ctx, "v1.0.0")
    if err != nil {
        log.Fatal(err)
    }

    config, err := img.ConfigFile()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Image labels: %v\n", config.Config.Labels)

    // Retag without re-uploading layers
    if err := moduleClient.TagImage(ctx, "v1.0.0", "latest"); err != nil {
        log.Fatal(err)
    }

    // Copy image to another registry
    destClient := client.New("mirror.example.com",
        client.WithLoginPassword("user", "pass"),
    ).WithSegment("deckhouse", "modules", "my-module")

    if err := moduleClient.CopyImage(ctx, "v1.0.0", destClient, "v1.0.0"); err != nil {
        log.Fatal(err)
    }
}

Architecture

Package Layout
pkg/registry/
β”œβ”€β”€ client.go       # Client interface definition
β”œβ”€β”€ errors.go       # Sentinel errors (ErrImageNotFound)
β”œβ”€β”€ image.go        # Image, ManifestResult, Manifest, IndexManifest, Descriptor interfaces
β”œβ”€β”€ options.go      # Option interfaces (ImageGetOption, ImagePushOption, ListTagsOption, etc.)
β”œβ”€β”€ go.mod
β”œβ”€β”€ README.md
└── client/         # Concrete implementation
    β”œβ”€β”€ auth.go         # Docker config JSON parsing, credential extraction
    β”œβ”€β”€ client.go       # Client struct and all registry operations
    β”œβ”€β”€ image.go        # Image, ManifestResult, Manifest, IndexManifest, Descriptor structs
    β”œβ”€β”€ middleware.go    # TransportMiddleware, RoundTripperFunc, WithMiddleware
    └── options.go      # Options struct, functional options (With*), transport building
Interface Hierarchy

The package separates interfaces (top-level registry package) from implementations (client sub-package), allowing consumers to depend only on the interfaces.

registry.Client β€” Main entry point for all operations:

Client
β”œβ”€β”€ WithSegment(segments ...string) Client
β”œβ”€β”€ GetRegistry() string
β”œβ”€β”€ GetImage(ctx, tag, ...ImageGetOption) (Image, error)
β”œβ”€β”€ PushImage(ctx, tag, v1.Image, ...ImagePushOption) error
β”œβ”€β”€ PushIndex(ctx, tag, v1.ImageIndex, ...ImagePushOption) error
β”œβ”€β”€ GetDigest(ctx, tag) (*v1.Hash, error)
β”œβ”€β”€ GetManifest(ctx, tag) (ManifestResult, error)
β”œβ”€β”€ GetImageConfig(ctx, tag) (*v1.ConfigFile, error)
β”œβ”€β”€ CheckImageExists(ctx, tag) error
β”œβ”€β”€ ListTags(ctx, ...ListTagsOption) ([]string, error)
β”œβ”€β”€ ListRepositories(ctx, ...ListRepositoriesOption) ([]string, error)
β”œβ”€β”€ DeleteTag(ctx, tag) error
β”œβ”€β”€ DeleteByDigest(ctx, v1.Hash) error
β”œβ”€β”€ TagImage(ctx, sourceTag, destTag) error
└── CopyImage(ctx, srcTag, dest Client, destTag) error

registry.Image β€” extends v1.Image with extraction:

Image (embeds v1.Image)
└── Extract() io.ReadCloser

registry.ManifestResult β€” wraps manifest or index manifest:

ManifestResult
β”œβ”€β”€ GetMediaType() types.MediaType
β”œβ”€β”€ GetManifest() (Manifest, error)
β”œβ”€β”€ GetIndexManifest() (IndexManifest, error)
└── GetDescriptor() Descriptor

registry.Manifest / registry.IndexManifest / registry.Descriptor β€” typed manifest access.

Core Concepts

Path Segmentation

One of the most powerful features is the ability to build repository paths through chainable WithSegment() calls. Each call creates a new client scoped to that path (the original client is unchanged):

// Start with base registry
base := client.New("registry.example.com", opts...)
// Path: registry.example.com

// Add organization
org := base.WithSegment("myorg")
// Path: registry.example.com/myorg

// Add project
project := org.WithSegment("myproject")
// Path: registry.example.com/myorg/myproject

// Add component
component := project.WithSegment("mycomponent")
// Path: registry.example.com/myorg/myproject/mycomponent

You can also add multiple segments at once:

// Single call with multiple segments
component := base.WithSegment("myorg", "myproject", "mycomponent")
// Path: registry.example.com/myorg/myproject/mycomponent

Segments are trimmed of leading/trailing slashes. Empty segment lists return the same client.

Creating a Client

Using Functional Options (Preferred)
import (
    "time"

    "github.com/google/go-containerregistry/pkg/authn"

    "github.com/deckhouse/deckhouse/pkg/log"
    "github.com/deckhouse/deckhouse/pkg/registry/client"
)

// Anonymous / default keychain
registryClient := client.New("registry.example.com",
    client.WithLogger(log.NewLogger().Named("registry")),
)

// With explicit credentials
registryClient := client.New("registry.example.com",
    client.WithLoginPassword("myuser", "mypassword"),
    client.WithLogger(log.NewLogger().Named("registry")),
)

// With TLS options and timeout
registryClient := client.New("registry.example.com",
    client.WithAuth(auth),
    client.WithTLSSkipVerify(true),
    client.WithTimeout(30*time.Second),
    client.WithLogger(log.NewLogger().Named("registry")),
)
Using the Options Struct
import (
    "github.com/deckhouse/deckhouse/pkg/log"
    "github.com/deckhouse/deckhouse/pkg/registry/client"
)

logger := log.NewLogger().Named("registry")

opts := &client.Options{
    Logger: logger,
}

registryClient := client.NewClientWithOptions("registry.example.com", opts)
With Authentication
import "github.com/google/go-containerregistry/pkg/authn"

// Basic authentication
auth := authn.FromConfig(authn.AuthConfig{
    Username: "myuser",
    Password: "mypassword",
})

opts := &client.Options{
    Auth:   auth,
    Logger: logger,
}

registryClient := client.NewClientWithOptions("registry.example.com", opts)
With Token Authentication
// Token-based authentication (e.g., Deckhouse license)
auth := authn.FromConfig(authn.AuthConfig{
    Username:      "json_key",
    Password:      "my-deckhouse-license-token",
    IdentityToken: "my-token",
})

opts := &client.Options{
    Auth:   auth,
    Logger: logger,
}

registryClient := client.NewClientWithOptions("registry.deckhouse.io", opts)
With Keychain
// Use a custom keychain (e.g., Kubernetes service-account keychain)
// Keychain is used only when Auth is nil.
registryClient := client.New("registry.example.com",
    client.WithKeychain(myKeychain),
)
From a Docker config JSON

WithDockercfg parses a raw or base64-encoded dockerconfig.json and extracts credentials for the target repository. Returns authn.Anonymous when the matching entry has empty username and password.

dockercfgOpt, err := client.WithDockercfg("registry.example.com", dockerCfgBase64)
if err != nil {
    log.Fatal(err)
}

registryClient := client.New("registry.example.com", dockercfgOpt)
With TLS Configuration
// Skip TLS verification (for testing)
registryClient := client.New("registry.example.com",
    client.WithAuth(auth),
    client.WithTLSSkipVerify(true),
)

// Custom CA certificate
registryClient := client.New("registry.example.com",
    client.WithAuth(auth),
    client.WithCA(caPEM),
)

// Use insecure HTTP
registryClient := client.New("registry.example.com",
    client.WithInsecure(true),
)

Authentication

The package supports multiple authentication strategies. Auth takes precedence over Keychain; if neither is set, authn.DefaultKeychain is used.

Method Function Description
Explicit authenticator WithAuth(auth) Any authn.Authenticator implementation
Username / password WithLoginPassword(u, p) Convenience wrapper around authn.Basic
Docker config JSON WithDockercfg(repo, cfg) Parses raw or base64-encoded config
Keychain WithKeychain(kc) Custom authn.Keychain (used when Auth is nil)
import "github.com/google/go-containerregistry/pkg/authn"

// Basic authentication
auth := authn.FromConfig(authn.AuthConfig{
    Username: "myuser",
    Password: "mypassword",
})
registryClient := client.New("registry.example.com", client.WithAuth(auth))

// Convenience helper – equivalent to the above
registryClient := client.New("registry.example.com",
    client.WithLoginPassword("myuser", "mypassword"),
)

// Token-based authentication
auth = authn.FromConfig(authn.AuthConfig{
    IdentityToken: "my-token",
})

// OAuth2 token
auth = authn.FromConfig(authn.AuthConfig{
    RegistryToken: "oauth2-token",
})

// Anonymous access β€” use anonymous authenticator
registryClient := client.New("registry.example.com",
    client.WithAuth(authn.Anonymous),
)

Image Operations

Pull an Image
// Pull by tag
img, err := registryClient.GetImage(ctx, "v1.0.0")
if err != nil {
    log.Fatal(err)
}

// Pull by digest
img, err := registryClient.GetImage(ctx, "@sha256:abc123...")
if err != nil {
    log.Fatal(err)
}

// Get the reference string used to pull the image
// Requires type assertion to *client.Image
fmt.Printf("Pull reference: %s\n", img.(*client.Image).GetPullReference())
Push an Image
import v1 "github.com/google/go-containerregistry/pkg/v1"

var imageToUpload v1.Image

err := registryClient.PushImage(ctx, "v1.0.1", imageToUpload)
if err != nil {
    log.Fatal(err)
}
Push an Image Index (Multi-Arch)

Push a manifest list / OCI image index that references platform-specific images:

import v1 "github.com/google/go-containerregistry/pkg/v1"

var idx v1.ImageIndex

err := registryClient.PushIndex(ctx, "v1.0.0", idx)
if err != nil {
    log.Fatal(err)
}
Get Image Digest

Uses HEAD first, falling back to GET if HEAD is unsupported or returns 404:

digest, err := registryClient.GetDigest(ctx, "v1.0.0")
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Digest: %s\n", digest.String())
Get Image Manifest

The GetManifest() method returns a ManifestResult that can represent either a standard manifest or an index manifest (for multi-architecture images).

manifestResult, err := registryClient.GetManifest(ctx, "v1.0.0")
if err != nil {
    log.Fatal(err)
}

// Get the descriptor (contains media type, size, digest)
descriptor := manifestResult.GetDescriptor()
fmt.Printf("Media Type: %s\n", descriptor.GetMediaType())
fmt.Printf("Size: %d bytes\n", descriptor.GetSize())
fmt.Printf("Digest: %s\n", descriptor.GetDigest())

// Check if it's an index manifest (multi-arch image)
if descriptor.GetMediaType().IsIndex() {
    // Handle index manifest (multi-platform)
    indexManifest, err := manifestResult.GetIndexManifest()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Schema Version: %d\n", indexManifest.GetSchemaVersion())

    // List all platform-specific manifests
    for _, manifest := range indexManifest.GetManifests() {
        platform := manifest.GetPlatform()
        if platform != nil {
            fmt.Printf("Platform: %s/%s\n", platform.OS, platform.Architecture)
            fmt.Printf("  Digest: %s\n", manifest.GetDigest())
            fmt.Printf("  Size: %d bytes\n", manifest.GetSize())
        }
    }
} else {
    // Handle regular manifest (single platform)
    manifest, err := manifestResult.GetManifest()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Schema Version: %d\n", manifest.GetSchemaVersion())

    // Access config
    config := manifest.GetConfig()
    fmt.Printf("Config Digest: %s\n", config.GetDigest())

    // Access layers
    for i, layer := range manifest.GetLayers() {
        fmt.Printf("Layer %d: %s (%d bytes)\n", i, layer.GetDigest(), layer.GetSize())
    }

    // Access annotations
    for key, value := range manifest.GetAnnotations() {
        fmt.Printf("Annotation %s: %s\n", key, value)
    }

    // Access subject (OCI referrers)
    if subject := manifest.GetSubject(); subject != nil {
        fmt.Printf("Subject: %s\n", subject.GetDigest())
    }
}

ManifestResult implementation detail (client.ManifestResult):

  • IsIndex() bool β€” convenience check for index manifests
  • Raw manifest bytes are lazily decoded on first call to GetManifest() or GetIndexManifest()
  • Calling GetManifest() on an index returns client.ErrIsIndexManifest
  • Calling GetIndexManifest() on a non-index returns client.ErrIsNotIndexManifest
  • client.NewManifestResultFromBytes(manifestBytes) constructs a result from raw JSON
Get Image Configuration
config, err := registryClient.GetImageConfig(ctx, "v1.0.0")
if err != nil {
    log.Fatal(err)
}

// Access metadata
fmt.Printf("Architecture: %s\n", config.Architecture)
fmt.Printf("OS: %s\n", config.OS)
fmt.Printf("Created: %s\n", config.Created.Time)

// Access labels
for key, value := range config.Config.Labels {
    fmt.Printf("Label %s: %s\n", key, value)
}
Check Image Existence

Uses HEAD first, falling back to GET if HEAD fails (for registries that don't support HEAD on manifests):

import "github.com/deckhouse/deckhouse/pkg/registry"

err := registryClient.CheckImageExists(ctx, "v1.0.0")
if errors.Is(err, registry.ErrImageNotFound) {
    fmt.Println("Image not found")
} else if err != nil {
    log.Fatal(err)
} else {
    fmt.Println("Image exists")
}
Extract Image Content
// Pull the image
img, err := registryClient.GetImage(ctx, "v1.0.0")
if err != nil {
    log.Fatal(err)
}

// Extract flattened layers as tar archive
reader := img.Extract()
defer reader.Close()

// Process the tar archive
// Contains all layers flattened into a single stream

Tag and Lifecycle Operations

Tag an Image (Retag)

Add a new tag pointing to the same manifest as an existing tag β€” a single manifest PUT with no layer re-upload:

// Promote v1.0.0 to latest
err := registryClient.TagImage(ctx, "v1.0.0", "latest")
if err != nil {
    log.Fatal(err)
}
Delete a Tag
err := registryClient.DeleteTag(ctx, "v1.0.0")
if err != nil {
    if errors.Is(err, registry.ErrImageNotFound) {
        fmt.Println("Tag does not exist")
    } else {
        log.Fatal(err)
    }
}
Delete by Digest

Delete a manifest by its digest, removing all tags that reference it:

import v1 "github.com/google/go-containerregistry/pkg/v1"

digest, _ := v1.NewHash("sha256:abc123...")

err := registryClient.DeleteByDigest(ctx, digest)
if err != nil {
    if errors.Is(err, registry.ErrImageNotFound) {
        fmt.Println("Manifest does not exist")
    } else {
        log.Fatal(err)
    }
}
Copy an Image

Copy an image between registries without pulling layers locally when possible. When both source and destination are *client.Client, server-side mount is used. Multi-arch indexes are handled automatically.

sourceClient := client.New("source.example.com",
    client.WithLoginPassword("user", "pass"),
).WithSegment("org", "project")

destClient := client.New("dest.example.com",
    client.WithLoginPassword("user", "pass"),
).WithSegment("mirror", "org", "project")

// Copies image including all layers
err := sourceClient.CopyImage(ctx, "v1.0.0", destClient, "v1.0.0")
if err != nil {
    log.Fatal(err)
}

Fallback behavior: If the destination is an interface registry.Client rather than a concrete *client.Client, the image is pulled and re-pushed via PushImage.

Repository Operations

List Tags

The ListTags method supports server-side pagination for large repositories:

import "github.com/deckhouse/deckhouse/pkg/registry/client"

// List all tags (auto-paginates internally)
tags, err := registryClient.ListTags(ctx)
if err != nil {
    log.Fatal(err)
}

// List first 50 tags (single page)
tags, err := registryClient.ListTags(ctx, client.WithTagsLimit(50))
if err != nil {
    log.Fatal(err)
}

for _, tag := range tags {
    fmt.Printf("Tag: %s\n", tag)
}

// Manual pagination
if len(tags) == 50 {
    nextTags, err := registryClient.ListTags(ctx,
        client.WithTagsLimit(50),
        client.WithTagsLast(tags[len(tags)-1]),
    )
    // Process next page...
}

Available Options:

Function Description
client.WithTagsLimit(n) Cap results to n tags (single page)
client.WithTagsLast(tag) Continue from a specific tag (pagination cursor)

Implementation detail: When pagination options are set, the client uses direct HTTP requests (with authenticated transport) against the /v2/<repo>/tags/list endpoint, following Link headers for multi-page results. Response bodies are limited to 8 MiB. Without options, remote.List() is used instead.

List Repositories

The ListRepositories method supports server-side pagination:

import "github.com/deckhouse/deckhouse/pkg/registry/client"

// List all repositories
repos, err := registryClient.ListRepositories(ctx)
if err != nil {
    log.Fatal(err)
}

// With pagination
repos, err := registryClient.ListRepositories(ctx, client.WithReposLimit(100))
if err != nil {
    log.Fatal(err)
}

// Continue pagination
if len(repos) == 100 {
    nextRepos, err := registryClient.ListRepositories(ctx,
        client.WithReposLimit(100),
        client.WithReposLast(repos[len(repos)-1]),
    )
    // Process next page...
}

Available Options:

Function Description
client.WithReposLimit(n) Cap results to n repos (single page via CatalogPage)
client.WithReposLast(repo) Continue from a specific repository (pagination cursor)

Implementation detail: Uses remote.CatalogPage when pagination options are set, falls back to remote.Catalog otherwise.

Advanced Usage

Transport Middlewares

The package supports a composable transport middleware chain for cross-cutting concerns like metrics, tracing, logging, or rate-limiting:

import (
    "net/http"
    "github.com/deckhouse/deckhouse/pkg/registry/client"
)

// Define a middleware using the TransportMiddleware type
func loggingMiddleware(next http.RoundTripper) http.RoundTripper {
    return client.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
        log.Printf("-> %s %s", req.Method, req.URL)
        resp, err := next.RoundTrip(req)
        if err == nil {
            log.Printf("<- %d %s", resp.StatusCode, req.URL)
        }
        return resp, err
    })
}

func metricsMiddleware(next http.RoundTripper) http.RoundTripper {
    return client.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
        start := time.Now()
        resp, err := next.RoundTrip(req)
        duration := time.Since(start)
        recordMetric(req.Method, req.URL.Path, duration)
        return resp, err
    })
}

// Apply middlewares β€” first middleware wraps the outermost layer
registryClient := client.New("registry.example.com",
    client.WithMiddleware(metricsMiddleware, loggingMiddleware),
    client.WithAuth(auth),
)

client.RoundTripperFunc is an adapter that allows ordinary functions to be used as http.RoundTripper, similar to http.HandlerFunc.

Middlewares can also be set via the Options struct:

opts := &client.Options{
    Auth:        auth,
    Middlewares: []client.TransportMiddleware{metricsMiddleware, loggingMiddleware},
}
registryClient := client.NewClientWithOptions("registry.example.com", opts)
Custom Transport

Provide a fully custom http.RoundTripper. When set, CA, TLSSkipVerify, Insecure, and ProxyURL transport-level settings are ignored (a warning is logged):

customTransport := &http.Transport{
    // ... your custom settings
}

registryClient := client.New("registry.example.com",
    client.WithCustomTransport(customTransport),
    client.WithAuth(auth),
)
Proxy Configuration

Set an explicit HTTP/HTTPS proxy for registry requests. Overrides any proxy configured via environment variables (HTTP_PROXY / HTTPS_PROXY). Pass nil to disable proxying entirely:

import "net/url"

proxyURL, _ := url.Parse("http://proxy.internal:3128")

registryClient := client.New("registry.example.com",
    client.WithProxy(proxyURL),
    client.WithAuth(auth),
)
Platform-Specific Image Retrieval

When working with multi-architecture images, specify the platform to retrieve the correct variant using the WithPlatform option:

import (
    v1 "github.com/google/go-containerregistry/pkg/v1"
    "github.com/deckhouse/deckhouse/pkg/registry/client"
)

platform := &v1.Platform{
    OS:           "linux",
    Architecture: "arm64",
}

img, err := registryClient.GetImage(ctx, "v1.0.0", client.WithPlatform{Platform: platform})
if err != nil {
    log.Fatal(err)
}

Common platforms:

// Linux AMD64
&v1.Platform{OS: "linux", Architecture: "amd64"}

// Linux ARM64
&v1.Platform{OS: "linux", Architecture: "arm64"}

// Linux ARM v7
&v1.Platform{OS: "linux", Architecture: "arm", Variant: "v7"}

Note: If no platform is specified, the registry typically returns the manifest for the host's native platform.

Working with Context

All operations support context for cancellation and timeouts:

import "time"

// With timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

img, err := registryClient.GetImage(ctx, "v1.0.0")
if err != nil {
    if ctx.Err() == context.DeadlineExceeded {
        log.Println("Operation timed out")
    }
}

// With cancellation
ctx, cancel := context.WithCancel(context.Background())

go func() {
    time.Sleep(5 * time.Second)
    cancel()
}()

tags, err := registryClient.ListTags(ctx)
if err != nil && ctx.Err() == context.Canceled {
    log.Println("Operation canceled")
}

Note: The client also supports Options.Timeout / WithTimeout(d), which applies an automatic context.WithTimeout wrapper around every operation.

Concurrent Operations

The client is thread-safe and can be used concurrently:

import "sync"

func processTags(ctx context.Context, c registry.Client, tags []string) {
    var wg sync.WaitGroup

    for _, tag := range tags {
        wg.Add(1)
        go func(t string) {
            defer wg.Done()

            digest, err := c.GetDigest(ctx, t)
            if err != nil {
                log.Printf("Failed to get digest for %s: %v", t, err)
                return
            }

            fmt.Printf("Tag %s: %s\n", t, digest)
        }(tag)
    }

    wg.Wait()
}
Working with Image Layers
img, err := registryClient.GetImage(ctx, "v1.0.0")
if err != nil {
    log.Fatal(err)
}

layers, err := img.Layers()
if err != nil {
    log.Fatal(err)
}

for i, layer := range layers {
    digest, _ := layer.Digest()
    size, _ := layer.Size()

    fmt.Printf("Layer %d: %s (%d bytes)\n", i, digest, size)
}

Configuration Options

Options Struct

The Options struct provides comprehensive configuration:

type Options struct {
    // Authentication β€” Auth takes precedence over Keychain.
    // If neither is set, authn.DefaultKeychain is used.
    Auth     authn.Authenticator // Explicit authenticator
    Keychain authn.Keychain      // Custom keychain (alternative to Auth)

    // HTTP / TLS
    Insecure      bool              // Use plain HTTP instead of HTTPS
    TLSSkipVerify bool              // Skip TLS certificate verification
    CA            string            // PEM-encoded custom CA certificate
    Scheme        string            // "http" or "https" (deprecated: prefer Insecure)

    // Transport
    Transport   http.RoundTripper      // Custom transport (overrides CA/TLS/Insecure/Proxy)
    ProxyURL    *url.URL               // Explicit proxy URL (overrides env vars)
    Middlewares []TransportMiddleware   // Transport middleware chain

    // Request behaviour
    UserAgent string        // User-Agent header value
    Timeout   time.Duration // Per-operation timeout (0 = no limit)

    // Logging
    Logger *log.Logger // Custom logger (auto-created as "registry-client" if nil)
}
Functional Options

All functional options are passed to client.New():

Function Signature Description
WithAuth (authn.Authenticator) Set an explicit authenticator
WithKeychain (authn.Keychain) Set a custom keychain
WithLoginPassword (user, pass string) Set Basic auth credentials
WithDockercfg (repo, cfg string) (Option, error) Parse Docker config JSON
WithInsecure (bool) Enable plain HTTP
WithTLSSkipVerify (bool) Disable TLS verification
WithCA (string) Set a PEM-encoded custom CA certificate
WithUserAgent (string) Set the User-Agent header
WithTimeout (time.Duration) Set per-operation timeout
WithLogger (*log.Logger) Set the logger
WithCustomTransport (http.RoundTripper) Set a custom HTTP transport
WithProxy (*url.URL) Set an explicit proxy URL
WithMiddleware (...TransportMiddleware) Add transport middlewares
WithScheme (string) Set URL scheme (deprecated: use WithInsecure)
Transport Constants

The default transport uses these sensible defaults:

Constant Value
defaultTimeout (dial/keep-alive) 120 s
defaultMaxIdleConns 100
defaultIdleConnTimeout 90 s
defaultTLSHandshakeTimeout 10 s
defaultExpectContinueTimeout 1 s
Complete Example
import (
    "net/url"
    "time"

    "github.com/google/go-containerregistry/pkg/authn"

    "github.com/deckhouse/deckhouse/pkg/log"
    "github.com/deckhouse/deckhouse/pkg/registry/client"
)

logger := log.NewLogger().Named("registry")
proxyURL, _ := url.Parse("http://proxy.internal:3128")

// Functional options style (preferred)
registryClient := client.New("registry.example.com",
    client.WithLoginPassword("myuser", "mypassword"),
    client.WithCA(caPEM),
    client.WithTimeout(2*time.Minute),
    client.WithProxy(proxyURL),
    client.WithMiddleware(metricsMiddleware),
    client.WithLogger(logger),
)

// Equivalent using Options struct
auth := authn.FromConfig(authn.AuthConfig{
    Username: "myuser",
    Password: "mypassword",
})

opts := &client.Options{
    Auth:        auth,
    CA:          caPEM,
    Timeout:     2 * time.Minute,
    ProxyURL:    proxyURL,
    Middlewares: []client.TransportMiddleware{metricsMiddleware},
    Logger:      logger,
}

registryClient = client.NewClientWithOptions("registry.example.com", opts)

Error Handling

Sentinel Errors
Error Package Description
ErrImageNotFound registry and client Image tag or digest does not exist
ErrIsIndexManifest client GetManifest() called on an index manifest
ErrIsNotIndexManifest client GetIndexManifest() called on a non-index manifest
import (
    "errors"

    "github.com/deckhouse/deckhouse/pkg/registry"
)

err := registryClient.CheckImageExists(ctx, "v1.0.0")
if errors.Is(err, registry.ErrImageNotFound) {
    fmt.Println("Image not found")
} else if err != nil {
    log.Fatal(err)
}
Transport Errors
import "github.com/google/go-containerregistry/pkg/v1/remote/transport"

img, err := registryClient.GetImage(ctx, "v1.0.0")
if err != nil {
    var transportErr *transport.Error
    if errors.As(err, &transportErr) {
        switch transportErr.StatusCode {
        case 401:
            log.Println("Authentication failed")
        case 403:
            log.Println("Access forbidden")
        case 404:
            log.Println("Image not found")
        case 500:
            log.Println("Registry server error")
        }
    }
}
Graceful Error Handling
// Try multiple tags with fallback
tags := []string{"latest", "stable", "v1.0.0"}

var img registry.Image
var err error

for _, tag := range tags {
    img, err = registryClient.GetImage(ctx, tag)
    if err == nil {
        fmt.Printf("Successfully pulled: %s\n", tag)
        break
    }

    if errors.Is(err, registry.ErrImageNotFound) {
        continue // Try next tag
    }

    log.Fatal(err) // Fatal error
}

Best Practices

1. Use Path Segmentation
// Good: Build paths incrementally for flexibility
base := client.New("registry.example.com", opts...)
org := base.WithSegment("myorg")
project := org.WithSegment("myproject")

// Avoid: Hardcoding full paths (less flexible)
fullPath := base.WithSegment("myorg/project") // Treated as single segment
2. Reuse Client Instances
// Good: Create once, reuse
registryClient := client.New("registry.example.com", opts...)

for _, tag := range tags {
    digest, _ := registryClient.GetDigest(ctx, tag)
    // ...
}

// Avoid: Creating new clients repeatedly
for _, tag := range tags {
    c := client.New("registry.example.com", opts...)
    digest, _ := c.GetDigest(ctx, tag)
}
3. Always Use Context with Timeout
// Good: Reasonable timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()

img, err := registryClient.GetImage(ctx, "v1.0.0")

// Or use client-level timeout
registryClient := client.New("registry.example.com",
    client.WithTimeout(5*time.Minute),
)
4. Close Readers
// Good: Always close readers
img, _ := registryClient.GetImage(ctx, "v1.0.0")
reader := img.Extract()
defer reader.Close()
5. Handle Errors Appropriately
// Good: Distinguish between different errors
err := registryClient.CheckImageExists(ctx, "v1.0.0")
if errors.Is(err, registry.ErrImageNotFound) {
    useDefaultImage()
} else if err != nil {
    log.Fatal(err)
}
6. Use CopyImage for Mirroring
// Good: Server-side copy avoids pulling layers locally
err := sourceClient.CopyImage(ctx, "v1.0.0", destClient, "v1.0.0")

// Avoid: Manual pull + push (pulls all layers locally)
img, _ := sourceClient.GetImage(ctx, "v1.0.0")
destClient.PushImage(ctx, "v1.0.0", img)
7. Use TagImage for Promotion
// Good: Single manifest PUT, no layer upload
err := registryClient.TagImage(ctx, "v1.0.0", "latest")

// Avoid: Pull + push just to retag
img, _ := registryClient.GetImage(ctx, "v1.0.0")
registryClient.PushImage(ctx, "latest", img)

Examples

Mirror Images Between Registries
func mirrorImage(ctx context.Context, source, target registry.Client, tag string) error {
    return source.CopyImage(ctx, tag, target, tag)
}

// Usage
sourceClient := client.New("source.example.com", sourceOpts...).
    WithSegment("org", "project")

targetClient := client.New("target.example.com", targetOpts...).
    WithSegment("mirror", "org", "project")

err := mirrorImage(ctx, sourceClient, targetClient, "v1.0.0")
Synchronize Repository Tags
func syncTags(ctx context.Context, source, target registry.Client) error {
    sourceTags, err := source.ListTags(ctx)
    if err != nil {
        return err
    }

    targetTags, err := target.ListTags(ctx)
    if err != nil {
        return err
    }

    // Find missing tags
    targetSet := make(map[string]bool)
    for _, tag := range targetTags {
        targetSet[tag] = true
    }

    // Copy missing tags
    for _, tag := range sourceTags {
        if !targetSet[tag] {
            if err := source.CopyImage(ctx, tag, target, tag); err != nil {
                log.Printf("Failed to copy %s: %v", tag, err)
                continue
            }
            fmt.Printf("Copied: %s\n", tag)
        }
    }

    return nil
}
Inspect Image Metadata
func inspectImage(ctx context.Context, c registry.Client, tag string) error {
    config, err := c.GetImageConfig(ctx, tag)
    if err != nil {
        return err
    }

    fmt.Printf("Image: %s\n", tag)
    fmt.Printf("Architecture: %s\n", config.Architecture)
    fmt.Printf("OS: %s\n", config.OS)
    fmt.Printf("Created: %s\n", config.Created.Time)

    if len(config.Config.Labels) > 0 {
        fmt.Println("Labels:")
        for key, value := range config.Config.Labels {
            fmt.Printf("  %s: %s\n", key, value)
        }
    }

    return nil
}
Clean Up Old Tags
func cleanupOldTags(ctx context.Context, c registry.Client, keep int) error {
    tags, err := c.ListTags(ctx)
    if err != nil {
        return err
    }

    if len(tags) <= keep {
        return nil
    }

    // Delete oldest tags (assumes lexicographic ordering)
    for _, tag := range tags[:len(tags)-keep] {
        if err := c.DeleteTag(ctx, tag); err != nil {
            if errors.Is(err, registry.ErrImageNotFound) {
                continue // Already deleted
            }
            log.Printf("Failed to delete %s: %v", tag, err)
        }
    }

    return nil
}
Promote Image Between Environments
func promoteImage(ctx context.Context, c registry.Client, srcTag, envTag string) error {
    // Verify source exists
    if err := c.CheckImageExists(ctx, srcTag); err != nil {
        return fmt.Errorf("source image %s: %w", srcTag, err)
    }

    // Retag without re-uploading
    return c.TagImage(ctx, srcTag, envTag)
}

// Usage
err := promoteImage(ctx, registryClient, "v1.2.3", "production")

Troubleshooting

Authentication Failures (401 Unauthorized)

Problem: Getting 401 errors when accessing registry.

Solution: Verify credentials and authentication method:

auth := authn.FromConfig(authn.AuthConfig{
    Username: "correct-username",
    Password: "correct-password",
})

registryClient := client.New("registry.example.com",
    client.WithAuth(auth),
    client.WithLogger(logger), // Enable logging
)
TLS Certificate Verification Errors

Problem: Certificate verification failures.

Solution: For development/testing (not production):

registryClient := client.New("registry.example.com",
    client.WithTLSSkipVerify(true),
    client.WithLogger(logger),
)

Better Solution: Provide the CA certificate:

registryClient := client.New("registry.example.com",
    client.WithCA(caPEM),
)
Connection Timeouts

Problem: Operations hanging or timing out.

Solution: Use appropriate timeouts:

// Client-level timeout
registryClient := client.New("registry.example.com",
    client.WithTimeout(10*time.Minute),
)

// Or context-level timeout for large images
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()

img, err := registryClient.GetImage(ctx, "large-image:latest")
Image Not Found Errors

Problem: Cannot find expected images.

Solution: Check image existence before operations:

err := registryClient.CheckImageExists(ctx, "v1.0.0")
if errors.Is(err, registry.ErrImageNotFound) {
    log.Println("Image doesn't exist, check tag name")
} else if err != nil {
    log.Println("Error checking image:", err)
}
Registry Not Responding

Problem: Cannot connect to registry.

Solution: For HTTP registries (not HTTPS):

registryClient := client.New("registry.example.com",
    client.WithInsecure(true),
    client.WithLogger(logger),
)
Proxy Issues

Problem: Registry behind a corporate proxy.

Solution: Configure proxy explicitly:

proxyURL, _ := url.Parse("http://proxy.internal:3128")

registryClient := client.New("registry.example.com",
    client.WithProxy(proxyURL),
)
Debug Logging

Enable detailed logging to diagnose issues:

import "log/slog"

logger := log.NewLogger(
    log.WithLevel(slog.LevelDebug),
).Named("registry-debug")

registryClient := client.New("registry.example.com",
    client.WithLogger(logger),
)

// All operations will log detailed information including
// registry host, segments, tags, and operation results

License

Apache License 2.0

Contributing

Contributions are welcome! Please ensure:

  • Code follows existing patterns (interfaces in registry, implementations in client)
  • All operations are thread-safe
  • New options follow the functional option pattern (With* functions)
  • Tests are included
  • Documentation is updated

Documentation ΒΆ

Index ΒΆ

Constants ΒΆ

This section is empty.

Variables ΒΆ

View Source
var (
	// ErrImageNotFound is returned when a requested image tag or digest does not
	// exist in the registry.
	ErrImageNotFound = errors.New("image not found")
)

Sentinel errors returned by Client implementations.

Functions ΒΆ

This section is empty.

Types ΒΆ

type Client ΒΆ

type Client interface {
	// WithSegment creates a new client scoped to an additional path segment.
	// This method can be chained: client.WithSegment("org").WithSegment("repo").
	// Multiple segments can be passed at once: client.WithSegment("org", "repo").
	WithSegment(segments ...string) Client

	// GetRegistry returns the full registry path (host + segments).
	GetRegistry() string

	// GetImage retrieves a remote image by tag or digest reference.
	GetImage(ctx context.Context, tag string, opts ...ImageGetOption) (Image, error)

	// PushImage pushes a v1.Image to the registry at the specified tag.
	PushImage(ctx context.Context, tag string, img v1.Image, opts ...ImagePushOption) error

	// PushIndex pushes a v1.ImageIndex (multi-arch manifest list) to the registry.
	PushIndex(ctx context.Context, tag string, idx v1.ImageIndex, opts ...ImagePushOption) error

	// GetDigest returns the digest hash for the given tag or digest reference.
	GetDigest(ctx context.Context, tag string) (*v1.Hash, error)

	// GetManifest retrieves the manifest for a specific image reference.
	GetManifest(ctx context.Context, tag string) (ManifestResult, error)

	// GetImageConfig retrieves the image config file containing labels and metadata.
	GetImageConfig(ctx context.Context, tag string) (*v1.ConfigFile, error)

	// CheckImageExists checks whether an image exists in the registry.
	// Returns ErrImageNotFound if the image does not exist.
	CheckImageExists(ctx context.Context, tag string) error

	// ListTags returns tags for the repository built by WithSegment calls.
	ListTags(ctx context.Context, opts ...ListTagsOption) ([]string, error)

	// ListRepositories lists repositories visible from the registry.
	ListRepositories(ctx context.Context, opts ...ListRepositoriesOption) ([]string, error)

	// DeleteTag deletes a specific tag from the registry.
	DeleteTag(ctx context.Context, tag string) error

	// DeleteByDigest deletes a manifest by its digest from the registry.
	DeleteByDigest(ctx context.Context, digest v1.Hash) error

	// TagImage adds a new tag pointing to the same manifest as sourceTag.
	TagImage(ctx context.Context, sourceTag, destTag string) error

	// CopyImage copies an image from this client's repository to a destination
	// client's repository, without pulling layers locally when possible.
	CopyImage(ctx context.Context, srcTag string, dest Client, destTag string) error
}

Client defines the interface for interacting with container registries. Implementations must be safe for concurrent use.

type Descriptor ΒΆ

type Descriptor interface {
	GetMediaType() types.MediaType
	GetSize() int64
	GetDigest() v1.Hash
	GetData() []byte
	GetURLs() []string
	GetAnnotations() map[string]string
	GetPlatform() *v1.Platform
	GetArtifactType() string
}

Descriptor defines methods for accessing descriptor information

type Image ΒΆ

type Image interface {
	v1.Image
	Extract() io.ReadCloser
}

type ImageGetOption ΒΆ

type ImageGetOption interface {
	// ApplyToImageGet applies this configuration to the given image get options.
	ApplyToImageGet(*ImageGetOptions)
}

ImageGetOption is some configuration that modifies options for a get request.

type ImageGetOptions ΒΆ

type ImageGetOptions struct {
	Platform *v1.Platform
}

type ImagePushOption ΒΆ

type ImagePushOption interface {
	// ApplyToImagePush applies this configuration to the given image put options.
	ApplyToImagePush(*ImagePushOptions)
}

ImagePushOption is some configuration that modifies options for a put request.

type ImagePushOptions ΒΆ

type ImagePushOptions struct {
}

type IndexManifest ΒΆ

type IndexManifest interface {
	GetSchemaVersion() int64
	GetMediaType() types.MediaType
	GetManifests() []Descriptor
	GetAnnotations() map[string]string
	GetSubject() Descriptor
}

IndexManifestInterface defines methods for accessing index manifest information

type ListRepositoriesOption ΒΆ

type ListRepositoriesOption interface {
	// ApplyToListRepositories applies this configuration to the given list repositories options.
	ApplyToListRepositories(*ListRepositoriesOptions)
}

ListRepositoriesOption is some configuration that modifies options for a list repositories request.

type ListRepositoriesOptions ΒΆ

type ListRepositoriesOptions struct {
	// Last repository name for pagination continuation
	Last string
	// Maximum number of results to return (0 means no limit)
	N int
}

type ListTagsOption ΒΆ

type ListTagsOption interface {
	// ApplyToListTags applies this configuration to the given list tags options.
	ApplyToListTags(*ListTagsOptions)
}

ListTagsOption is some configuration that modifies options for a list tags request.

type ListTagsOptions ΒΆ

type ListTagsOptions struct {
	// Last tag for pagination continuation
	Last string
	// Maximum number of results to return (0 means no limit)
	N int
}

type Manifest ΒΆ

type Manifest interface {
	GetSchemaVersion() int64
	GetMediaType() types.MediaType
	GetConfig() Descriptor
	GetLayers() []Descriptor
	GetAnnotations() map[string]string
	GetSubject() Descriptor
}

ManifestInterface defines methods for accessing manifest information

type ManifestResult ΒΆ

type ManifestResult interface {
	GetMediaType() types.MediaType
	GetManifest() (Manifest, error)
	GetIndexManifest() (IndexManifest, error)
	GetDescriptor() Descriptor
}

Directories ΒΆ

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL