Skip to main content

OCI Client

How to push and pull blob archives to OCI container registries.

The blob package provides a high-level API for working with blob archives in OCI registries. It handles authentication, manifest management, and lazy blob access via HTTP range requests.

Creating a Client

Create a client with blob.NewClient():

import "github.com/meigma/blob"

c, err := blob.NewClient(blob.WithDockerConfig())
if err != nil {
return err
}

The client uses ORAS under the hood and supports all standard OCI registries including Docker Hub, GitHub Container Registry (ghcr.io), Amazon ECR, Google Artifact Registry, and Azure Container Registry.

Authentication Options

Read credentials from ~/.docker/config.json:

c, _ := blob.NewClient(blob.WithDockerConfig())

This is the recommended approach for development and CI environments where Docker is already configured.

Static Credentials

Provide username and password directly:

c, _ := blob.NewClient(
blob.WithStaticCredentials("ghcr.io", "username", "password"),
)

Static Token

Use a bearer token directly:

c, _ := blob.NewClient(
blob.WithStaticToken("ghcr.io", "your-token"),
)

Anonymous Access

For public registries that don't require authentication:

c, _ := blob.NewClient(blob.WithAnonymous())

Push Operations

Basic Push

Push a directory to a registry as an archive:

import "github.com/meigma/blob"

func pushArchive(srcDir string) error {
c, err := blob.NewClient(blob.WithDockerConfig())
if err != nil {
return err
}

return c.Push(ctx, "ghcr.io/myorg/myarchive:v1.0.0", srcDir,
blob.PushWithCompression(blob.CompressionZstd),
)
}

Multiple Tags

Apply additional tags to the same manifest:

err := c.Push(ctx, "ghcr.io/myorg/myarchive:v1.0.0", srcDir,
blob.PushWithTags("latest", "stable"),
)

Custom Annotations

Add OCI annotations to the manifest:

err := c.Push(ctx, "ghcr.io/myorg/myarchive:v1.0.0", srcDir,
blob.PushWithAnnotations(map[string]string{
"org.opencontainers.image.source": "https://github.com/myorg/myrepo",
"org.opencontainers.image.revision": "abc123",
}),
)

The org.opencontainers.image.created annotation is set automatically if not provided.

Pushing Pre-created Archives

If you have a pre-created archive from blobcore.CreateBlob:

import (
"github.com/meigma/blob"
blobcore "github.com/meigma/blob/core"
)

blobFile, _ := blobcore.CreateBlob(ctx, srcDir, destDir,
blobcore.CreateBlobWithCompression(blobcore.CompressionZstd),
)
defer blobFile.Close()

c, _ := blob.NewClient(blob.WithDockerConfig())
c.PushArchive(ctx, "ghcr.io/myorg/myarchive:v1.0.0", blobFile.Blob,
blob.PushWithTags("latest"),
)

Pull Operations

Basic Pull (Lazy Loading)

Pull returns a *blobcore.Blob with lazy data loading:

func readFromRegistry(ref string) error {
c, err := blob.NewClient(blob.WithDockerConfig())
if err != nil {
return err
}

archive, err := c.Pull(ctx, ref)
if err != nil {
return err
}

// Data is fetched on demand via HTTP range requests
content, err := archive.ReadFile("config.json")
if err != nil {
return err
}
fmt.Printf("Content: %s\n", content)

return nil
}

The pulled archive uses HTTP range requests to fetch file data on demand. Only the small index blob is downloaded immediately; file contents are fetched lazily when accessed.

Pull Options

Configure blob decoding and limits:

archive, err := c.Pull(ctx, ref,
blob.PullWithMaxFileSize(512 << 20), // 512 MB limit
blob.PullWithDecoderConcurrency(4), // Parallel decompression
blob.PullWithMaxIndexSize(16 << 20), // 16 MB index limit
)

Skip Cache

Force a fresh fetch bypassing all caches:

archive, err := c.Pull(ctx, ref,
blob.PullWithSkipCache(),
)

Fetch Operations (Metadata Only)

Use Fetch to retrieve manifest metadata without downloading data:

manifest, err := c.Fetch(ctx, "ghcr.io/myorg/myarchive:v1.0.0")
if err != nil {
return err
}

fmt.Printf("Digest: %s\n", manifest.Digest())
fmt.Printf("Index size: %d bytes\n", manifest.IndexDescriptor().Size)
fmt.Printf("Data size: %d bytes\n", manifest.DataDescriptor().Size)

This is useful for checking if an archive exists or inspecting its metadata without the overhead of setting up lazy blob access.

Skip Cache on Fetch

manifest, err := c.Fetch(ctx, ref,
blob.FetchWithSkipCache(),
)

Inspect Operations (Metadata + File Index)

Use Inspect to retrieve both manifest metadata and the file index without downloading the data blob. This is more comprehensive than Fetch and lets you examine archive contents:

result, err := c.Inspect(ctx, "ghcr.io/myorg/myarchive:v1.0.0")
if err != nil {
return err
}

// Access manifest metadata
fmt.Printf("Digest: %s\n", result.Digest())
fmt.Printf("Created: %s\n", result.Created())

// Access archive statistics
fmt.Printf("Files: %d\n", result.FileCount())
fmt.Printf("Data blob size: %d bytes\n", result.DataBlobSize())
fmt.Printf("Uncompressed size: %d bytes\n", result.TotalUncompressedSize())
fmt.Printf("Compression ratio: %.2f\n", result.CompressionRatio())

// List all files without downloading any data
for entry := range result.Index().Entries() {
fmt.Printf(" %s (%d bytes)\n", entry.Path(), entry.OriginalSize())
}

Fetch vs Inspect vs Pull

OperationDownloadsUse Case
FetchManifest onlyCheck if archive exists, get digest
InspectManifest + file indexBrowse files, check sizes, pre-flight validation
PullManifest + index (+ data on demand)Read file contents

Fetching Referrers (Signatures, Attestations)

Inspect provides lazy access to referrer artifacts like Sigstore signatures and SLSA attestations:

result, err := c.Inspect(ctx, ref)
if err != nil {
return err
}

// Fetch all referrers
referrers, err := result.Referrers(ctx, "")
if err != nil {
if errors.Is(err, blob.ErrReferrersUnsupported) {
fmt.Println("Registry does not support referrers API")
}
return err
}

for _, ref := range referrers {
fmt.Printf("Referrer: %s (type: %s)\n", ref.Digest, ref.ArtifactType)
}

// Filter by artifact type
signatures, _ := result.Referrers(ctx, "application/vnd.dev.sigstore.bundle.v0.3+json")
fmt.Printf("Found %d signatures\n", len(signatures))

Inspect Options

// Skip all caches for fresh data
result, err := c.Inspect(ctx, ref,
blob.InspectWithSkipCache(),
)

// Limit index size (for untrusted registries)
result, err := c.Inspect(ctx, ref,
blob.InspectWithMaxIndexSize(4 << 20), // 4 MB limit
)

Tag Operations

Create or update a tag pointing to an existing manifest:

// First, fetch the manifest to get its digest
manifest, err := c.Fetch(ctx, "ghcr.io/myorg/myarchive:v1.0.0")
if err != nil {
return err
}

// Tag the same manifest with a new tag
err = c.Tag(ctx, "ghcr.io/myorg/myarchive:latest", manifest.Digest())

Note: For tagging during push, use blob.PushWithTags() which is more reliable as it applies tags atomically with the push operation.

Sign Operations

Sign a manifest with Sigstore and attach the signature as an OCI 1.1 referrer:

import "github.com/meigma/blob/policy/sigstore"

// Create a signer (keyless, for CI environments)
signer, err := sigstore.NewSigner(
sigstore.WithEphemeralKey(),
sigstore.WithFulcio("https://fulcio.sigstore.dev"),
sigstore.WithRekor("https://rekor.sigstore.dev"),
sigstore.WithAmbientCredentials(),
)
if err != nil {
return err
}

// Sign the manifest (after pushing)
sigDigest, err := c.Sign(ctx, "ghcr.io/myorg/myarchive:v1.0.0", signer)
if err != nil {
return err
}
fmt.Printf("Signature digest: %s\n", sigDigest)

The signature is attached as an OCI referrer artifact. Consumers can discover it via the Referrers API and verify it using a sigstore.Policy.

Complete Push and Sign Workflow

func pushAndSign(ctx context.Context, ref, srcDir string) error {
c, err := blob.NewClient(blob.WithDockerConfig())
if err != nil {
return err
}

// Push the archive
if err := c.Push(ctx, ref, srcDir,
blob.PushWithCompression(blob.CompressionZstd),
); err != nil {
return fmt.Errorf("push: %w", err)
}

// Create keyless signer
signer, err := sigstore.NewSigner(
sigstore.WithEphemeralKey(),
sigstore.WithFulcio("https://fulcio.sigstore.dev"),
sigstore.WithRekor("https://rekor.sigstore.dev"),
sigstore.WithAmbientCredentials(),
)
if err != nil {
return fmt.Errorf("create signer: %w", err)
}

// Sign the manifest
if _, err := c.Sign(ctx, ref, signer); err != nil {
return fmt.Errorf("sign: %w", err)
}

return nil
}

For detailed signing options and verification, see Provenance & Signing.

Error Handling

The client provides sentinel errors for common failure cases:

import "errors"

archive, err := c.Pull(ctx, ref)
if err != nil {
switch {
case errors.Is(err, blob.ErrNotFound):
// Archive does not exist at this reference
return fmt.Errorf("archive not found: %s", ref)

case errors.Is(err, blob.ErrInvalidReference):
// Reference string is malformed
return fmt.Errorf("invalid reference: %s", ref)

case errors.Is(err, blob.ErrInvalidManifest):
// Manifest exists but is not a valid blob archive
return fmt.Errorf("not a blob archive: %s", ref)

default:
return err
}
}

Available Errors

ErrorDescription
blob.ErrNotFoundArchive does not exist at the reference
blob.ErrInvalidReferenceReference string is malformed
blob.ErrInvalidManifestManifest is not a valid blob archive manifest
blob.ErrMissingIndexManifest does not contain an index blob
blob.ErrMissingDataManifest does not contain a data blob
blob.ErrPolicyViolationArchive rejected by a configured policy

Plain HTTP for Local Development

For local registries without TLS:

c, _ := blob.NewClient(
blob.WithDockerConfig(),
blob.WithPlainHTTP(true),
)

// Works with local registries like localhost:5000
err := c.Push(ctx, "localhost:5000/myarchive:latest", srcDir)

Complete Example

A complete workflow pushing and pulling with caching:

package main

import (
"context"
"fmt"
"log"

"github.com/meigma/blob"
)

func main() {
ctx := context.Background()

// Create client with all caches enabled
c, err := blob.NewClient(
blob.WithDockerConfig(),
blob.WithCacheDir("/var/cache/blob"),
)
if err != nil {
log.Fatal(err)
}

// Push an archive
ref := "ghcr.io/myorg/myarchive:v1.0.0"
if err := c.Push(ctx, ref, "./src",
blob.PushWithCompression(blob.CompressionZstd),
blob.PushWithTags("latest"),
); err != nil {
log.Fatal(err)
}
fmt.Printf("Pushed to %s\n", ref)

// Pull and read lazily
archive, err := c.Pull(ctx, ref)
if err != nil {
log.Fatal(err)
}

content, err := archive.ReadFile("main.go")
if err != nil {
log.Fatal(err)
}
fmt.Printf("main.go:\n%s\n", content)
}

See Also