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
Docker Config (Recommended)
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
| Operation | Downloads | Use Case |
|---|---|---|
Fetch | Manifest only | Check if archive exists, get digest |
Inspect | Manifest + file index | Browse files, check sizes, pre-flight validation |
Pull | Manifest + 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
| Error | Description |
|---|---|
blob.ErrNotFound | Archive does not exist at the reference |
blob.ErrInvalidReference | Reference string is malformed |
blob.ErrInvalidManifest | Manifest is not a valid blob archive manifest |
blob.ErrMissingIndex | Manifest does not contain an index blob |
blob.ErrMissingData | Manifest does not contain a data blob |
blob.ErrPolicyViolation | Archive 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
- Creating Archives - Archive creation options
- Caching - Cache configuration and sizing
- Provenance & Signing - Signing archives and verification policies
- OCI Storage - How blob archives are stored in OCI registries