Dagger
Search

oci

knows how to talk to an OCI registry and nothing about why.

It does not choose tags, decide when to publish, or know what the bytes it
uploads mean. Callers that need a registry — z5labs' App publish path,
ssdd's baselines — get one here instead of each growing their own.

The module is pure Go. Container.Publish cannot see session service
bindings, which is why callers used to shell out to a container that
could; a Go client running in the module's own runtime reaches a Dagger
service directly, so this wraps go-containerregistry and oras-go rather
than pinning tool images.

Installation

dagger install github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb

Entrypoint

Return Type
Oci
Example
dagger -m github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb call \
func (m *MyModule) Example() *dagger.Oci  {
	return dag.
			Oci()
}
@function
def example() -> dagger.Oci:
	return (
		dag.oci()
	)
@func()
example(): Oci {
	return dag
		.oci()
}

Types

Oci 🔗

Oci is the module’s entrypoint. It holds no state; every operation is reached through Registry.

credentialResolutionSelfTest() 🔗

CredentialResolutionSelfTest checks how a Docker config is read: which entry a host matches, which of an entry’s forms wins, when a credential helper is refused, and what a malformed config is allowed to say.

It sits on the module rather than in tests/ because it checks unexported resolution that never reaches the network, and because each case is one shape of a config file — reaching them all through tests/ would mean a registry per shape to assert on a string comparison. The live tests still prove a resolved credential authenticates; this proves the right one was resolved.

It runs in process and needs no container, so it is cheap enough to be a check of its own.

Return Type
Void !
Example
dagger -m github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb call \
 credential-resolution-self-test
func (m *MyModule) Example(ctx context.Context)   {
	return dag.
			Oci().
			Credentialresolutionselftest(ctx)
}
@function
async def example() -> None:
	return await (
		dag.oci()
		.credentialresolutionselftest()
	)
@func()
async example(): Promise<void> {
	return dag
		.oci()
		.credentialResolutionSelfTest()
}

registry() 🔗

Registry binds one registry host and its credentials. service, when non-nil, is a Dagger-hosted registry reached by hostname rather than over the public network — its endpoint replaces host as the address dialled, because a session service’s hostname is assigned by the engine and cannot be predicted by the caller.

There are three ways to authenticate and they have a fixed precedence: username/password beats bearerToken, which beats dockerConfig, and supplying none of them is an anonymous client. See Registry.credential for why the order is that one and why a 401 never falls through to the next source.

insecure is explicit and defaults to off: it means plain HTTP and no TLS verification. It is deliberately not inferred from service being set — that inference is a test affordance leaking into production behaviour. It is spelled insecure rather than tlsVerify because a bool defaulting to true is unsettable from the CLI.

caCert, clientCert and clientKey are the TLS material, and all three are independent of insecure. A registry fronted by a private CA is reached by naming that CA, with verification still on — turning verification off to work around a missing trust anchor is the outcome this exists to remove.

Return Type
Registry !
Arguments
NameTypeDefault ValueDescription
hostString !-

Registry host, as it appears in an image reference: “ghcr.io”, “registry.example.com:5000”. Ignored when service is set.

usernameString -

Username for basic authentication. Omit for an anonymous client.

passwordSecret -

Password or token for basic authentication.

bearerTokenSecret -

A bearer token to send as-is, for a registry that issued one. Used only when no username or password was given.

dockerConfigSecret -

A Docker config file — the contents of ~/.docker/config.json — to read this host’s credentials out of. Used only when nothing more specific was given. Credential helpers named by the file are not run; a host that resolves through one fails naming it.

serviceService -

A Dagger-hosted registry to reach over the session network instead of over the public network.

insecureBoolean -

Talk plain HTTP and skip TLS verification. Off by default.

caCertFile -

A PEM-encoded certificate authority to verify this registry’s certificate against, for a registry fronted by a private CA. It is added to the system trust store, not substituted for it, and it does not switch verification off.

clientCertFile -

A PEM-encoded client certificate to authenticate with, for a registry that authenticates callers by mutual TLS. Must be given together with clientKey.

clientKeySecret -

The PEM-encoded private key for clientCert. It crosses as a secret rather than a file because it is key material. Must be given together with clientCert.

Example
dagger -m github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb call \
 registry --host string
func (m *MyModule) Example(host string) *dagger.OciRegistry  {
	return dag.
			Oci().
			Registry(host)
}
@function
def example(host: str) -> dagger.OciRegistry:
	return (
		dag.oci()
		.registry(host)
	)
@func()
example(host: string): OciRegistry {
	return dag
		.oci()
		.registry(host)
}

Registry 🔗

Registry is an authenticated handle on one registry host.

Every method carries a never-cache directive on its own doc-comment line: registry state is mutable and pushes are side-effecting, so the directive repeats on each chained method rather than living only on the factory.

host() 🔗

Host is the registry host this handle was built for.

Return Type
String !
Example
dagger -m github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb call \
 registry --host string \
 host
func (m *MyModule) Example(ctx context.Context, host string) string  {
	return dag.
			Oci().
			Registry(host).
			Host(ctx)
}
@function
async def example(host: str) -> str:
	return await (
		dag.oci()
		.registry(host)
		.host()
	)
@func()
async example(host: string): Promise<string> {
	return dag
		.oci()
		.registry(host)
		.host()
}

username() 🔗

Username is the basic-auth user, empty for an anonymous client.

Return Type
String !
Example
dagger -m github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb call \
 registry --host string \
 username
func (m *MyModule) Example(ctx context.Context, host string) string  {
	return dag.
			Oci().
			Registry(host).
			Username(ctx)
}
@function
async def example(host: str) -> str:
	return await (
		dag.oci()
		.registry(host)
		.username()
	)
@func()
async example(host: string): Promise<string> {
	return dag
		.oci()
		.registry(host)
		.username()
}

insecure() 🔗

Insecure reports whether this handle talks plain HTTP.

Return Type
Boolean !
Example
dagger -m github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb call \
 registry --host string \
 insecure
func (m *MyModule) Example(ctx context.Context, host string) bool  {
	return dag.
			Oci().
			Registry(host).
			Insecure(ctx)
}
@function
async def example(host: str) -> bool:
	return await (
		dag.oci()
		.registry(host)
		.insecure()
	)
@func()
async example(host: string): Promise<boolean> {
	return dag
		.oci()
		.registry(host)
		.insecure()
}

attach() 🔗

Attach uploads content as an OCI referrer of subject and returns the referrer’s own digest.

subject is a manifest digest in this repository; it is resolved first, so attaching to something that is not there fails naming the digest rather than leaving a dangling referrer behind.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
repositoryString !-

Repository holding the subject. The referrer lands here too — the referrers API is per-repository.

subjectString !-

Digest of the manifest being attached to, e.g. “sha256:…”.

contentFile !-

The bytes to attach: one file, one layer.

artifactTypeString !-

The referrer’s artifact type, which is what Referrers filters on.

Example
dagger -m github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb call \
 registry --host string \
 attach --repository string --subject string --content file:path --artifact-type string
func (m *MyModule) Example(ctx context.Context, host string, repository string, subject string, content *dagger.File, artifactType string) string  {
	return dag.
			Oci().
			Registry(host).
			Attach(ctx, repository, subject, content, artifactType)
}
@function
async def example(host: str, repository: str, subject: str, content: dagger.File, artifacttype: str) -> str:
	return await (
		dag.oci()
		.registry(host)
		.attach(repository, subject, content, artifacttype)
	)
@func()
async example(host: string, repository: string, subject: string, content: File, artifactType: string): Promise<string> {
	return dag
		.oci()
		.registry(host)
		.attach(repository, subject, content, artifactType)
}

copy() 🔗

Copy copies srcRef into repository:tag on this registry, preserving every manifest — a multi-platform source stays multi-platform, matching what skopeo copy --all did before this module existed. It returns the digest at the destination.

The source is read with this registry’s credentials when it lives on this registry, and anonymously otherwise; cross-registry copies needing source credentials are a follow-up, not a silent reuse of the destination’s.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
srcRefString !-

Fully-qualified source reference, e.g. “docker.io/library/alpine:3.20” or “/@sha256:…”.

repositoryString !-

Destination repository on this registry.

tagString !-

Destination tag.

Example
dagger -m github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb call \
 registry --host string \
 copy --src-ref string --repository string --tag string
func (m *MyModule) Example(ctx context.Context, host string, srcRef string, repository string, tag string) string  {
	return dag.
			Oci().
			Registry(host).
			Copy(ctx, srcRef, repository, tag)
}
@function
async def example(host: str, srcref: str, repository: str, tag: str) -> str:
	return await (
		dag.oci()
		.registry(host)
		.copy(srcref, repository, tag)
	)
@func()
async example(host: string, srcRef: string, repository: string, tag: string): Promise<string> {
	return dag
		.oci()
		.registry(host)
		.copy(srcRef, repository, tag)
}

fetch() 🔗

Fetch downloads one blob or manifest by digest and returns it as a file.

Blobs are tried first and manifests second, because the two live at different registry endpoints and a caller holding a digest out of a manifest’s layer list has no reason to know which it is.

Return Type
File !
Arguments
NameTypeDefault ValueDescription
repositoryString !-

Repository holding the content.

digestString !-

Digest of the blob or manifest, e.g. “sha256:…”.

Example
dagger -m github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb call \
 registry --host string \
 fetch --repository string --digest string
func (m *MyModule) Example(host string, repository string, digest string) *dagger.File  {
	return dag.
			Oci().
			Registry(host).
			Fetch(repository, digest)
}
@function
def example(host: str, repository: str, digest: str) -> dagger.File:
	return (
		dag.oci()
		.registry(host)
		.fetch(repository, digest)
	)
@func()
example(host: string, repository: string, digest: string): File {
	return dag
		.oci()
		.registry(host)
		.fetch(repository, digest)
}

manifest() 🔗

Manifest returns the raw manifest JSON for a tag or a digest. It is raw rather than parsed because annotations, platforms and referrer subjects are all read back out of it, and re-encoding through a Go type would drop whatever this module does not model.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
repositoryString !-

Repository holding the manifest.

referenceString !-

Tag or digest.

Example
dagger -m github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb call \
 registry --host string \
 manifest --repository string --reference string
func (m *MyModule) Example(ctx context.Context, host string, repository string, reference string) string  {
	return dag.
			Oci().
			Registry(host).
			Manifest(ctx, repository, reference)
}
@function
async def example(host: str, repository: str, reference: str) -> str:
	return await (
		dag.oci()
		.registry(host)
		.manifest(repository, reference)
	)
@func()
async example(host: string, repository: string, reference: string): Promise<string> {
	return dag
		.oci()
		.registry(host)
		.manifest(repository, reference)
}

pushArtifact() 🔗

PushArtifact pushes the files in contents to repository:tag as an OCI artifact of artifactType, and returns the manifest digest.

Every file in contents, at any depth, becomes one layer whose org.opencontainers.image.title annotation is its path relative to the directory root. Layers are ordered by that path so the same directory always produces the same manifest.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
repositoryString !-

Repository path within the registry.

tagString !-

Tag to publish under.

contentsDirectory !-

Files to upload. Must contain at least one file.

artifactTypeString !-

The artifact’s type, e.g. “application/vnd.example.sbom.v1+json”. This is what a consumer filters on when listing referrers.

Example
dagger -m github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb call \
 registry --host string \
 push-artifact --repository string --tag string --contents DIR_PATH --artifact-type string
func (m *MyModule) Example(ctx context.Context, host string, repository string, tag string, contents *dagger.Directory, artifactType string) string  {
	return dag.
			Oci().
			Registry(host).
			Pushartifact(ctx, repository, tag, contents, artifactType)
}
@function
async def example(host: str, repository: str, tag: str, contents: dagger.Directory, artifacttype: str) -> str:
	return await (
		dag.oci()
		.registry(host)
		.pushartifact(repository, tag, contents, artifacttype)
	)
@func()
async example(host: string, repository: string, tag: string, contents: Directory, artifactType: string): Promise<string> {
	return dag
		.oci()
		.registry(host)
		.pushArtifact(repository, tag, contents, artifactType)
}

pushImage() 🔗

PushImage pushes container variants to repository:tag and returns the digest of what was pushed.

Multiple variants become one manifest list. The variants are materialized through a single AsTarball with the rest as platform variants, so the index this pushes is the one Dagger itself would have published — annotations, config and layer bytes included — rather than one this module reassembled.

repository and tag are separate parameters rather than one interpolated reference: it keeps caller-supplied values out of any string that gets re-parsed as something else, and it makes each half validatable.

A caller that has fallible work to do between the push and the moment the image becomes resolvable — attaching referrers, say — wants PushImageUntagged and Tag instead, which split this into its two halves.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
repositoryString !-

Repository path within the registry, e.g. “z5labs/myapp”.

tagString !-

Tag to publish under.

variants[Container ! ] !-

Platform variants. One variant pushes a single image manifest; more than one pushes a manifest list naming every platform.

Example
dagger -m github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb call \
 registry --host string \
 push-image --repository string --tag string
func (m *MyModule) Example(ctx context.Context, host string, repository string, tag string, variants []*dagger.Container) string  {
	return dag.
			Oci().
			Registry(host).
			Pushimage(ctx, repository, tag, variants)
}
@function
async def example(host: str, repository: str, tag: str, variants: List[dagger.Container]) -> str:
	return await (
		dag.oci()
		.registry(host)
		.pushimage(repository, tag, variants)
	)
@func()
async example(host: string, repository: string, tag: string, variants: Container[]): Promise<string> {
	return dag
		.oci()
		.registry(host)
		.pushImage(repository, tag, variants)
}

pushImageUntagged() 🔗

PushImageUntagged pushes container variants to repository under their own digest and no tag at all, and returns that digest.

The bytes land exactly as PushImage lands them — same manifest list, same blobs — but nothing in the repository names them, so nothing that resolves a tag can reach them. That is the point: a caller with fallible work to do against the pushed digest (attaching SBOMs, attaching provenance) can do it while the image is unreachable, and call Tag only once that work is done. A failure in between leaves an unreferenced manifest rather than a tag a consumer can pull.

Pushing a manifest by digest is the same registry operation the referrers path already relies on, so it needs nothing of a registry that Attach does not need already.

The manifest is unreferenced until it is tagged or something points at it, which means a registry running garbage collection is entitled to delete it. Registries collect on an operator-run sweep rather than continuously — it is offline and manual on distribution, and scheduled on GHCR — so the window this opens is not one a publish has to design around. A caller that leaves a digest untagged indefinitely is a caller relying on something no registry promises.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
repositoryString !-

Repository path within the registry, e.g. “z5labs/myapp”.

variants[Container ! ] !-

Platform variants. One variant pushes a single image manifest; more than one pushes a manifest list naming every platform.

Example
dagger -m github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb call \
 registry --host string \
 push-image-untagged --repository string
func (m *MyModule) Example(ctx context.Context, host string, repository string, variants []*dagger.Container) string  {
	return dag.
			Oci().
			Registry(host).
			Pushimageuntagged(ctx, repository, variants)
}
@function
async def example(host: str, repository: str, variants: List[dagger.Container]) -> str:
	return await (
		dag.oci()
		.registry(host)
		.pushimageuntagged(repository, variants)
	)
@func()
async example(host: string, repository: string, variants: Container[]): Promise<string> {
	return dag
		.oci()
		.registry(host)
		.pushImageUntagged(repository, variants)
}

pushLayer() 🔗

PushLayer pushes one file to repository:tag as a single-layer OCI image manifest and returns the manifest digest.

It exists because two families of consumer read a document’s meaning off the layer rather than off the manifest: they resolve a tag whose name they compute themselves, take the one layer whose media type they recognise, and read the rest out of that layer’s annotations. Cosign’s signature layout is the one this repository needs — sha256-<hex>.sig, one application/vnd.dev.cosign.simplesigning.v1+json layer, the signature in an annotation beside it — but nothing here knows that. This function is handed a tag, some bytes, a media type and a set of annotations, exactly as Attach is handed a file and an artifact type, and that is all it ever learns.

The three ways it differs from PushArtifact and Attach, each of which is why neither of those could be stretched to cover it:

  • The layer’s media type is the caller’s. PushArtifact gives every layer application/octet-stream, which is right for a document a consumer fetches by digest and wrong for one a consumer finds by filtering layers on their type.
  • The layer’s annotations are the caller’s. Both of the others set the standard title annotation and nothing else, so there is nowhere to put a signature.
  • The config is a real empty image config rather than the OCI empty descriptor oras.PackManifest would choose. Readers of this layout go through go-containerregistry’s image type, which expects an image manifest carrying an image config; the artifact-manifest shape is legal OCI and is not what they parse.

The manifest is addressed by tag, so pushing the same tag twice replaces it — which is what a caller re-signing a digest wants, and is the difference from Attach, where each call adds a referrer.

The content is held in memory whole, exactly as Attach and PushArtifact hold theirs, so this is sized for documents — signatures, payloads, attestations — and not for image layers. Streaming would be a change to all three rather than to this one, since a caller cannot tell them apart on that axis today.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
repositoryString !-

Repository to push to.

tagString !-

Tag to push under. Callers of this function normally compute it from a digest rather than taking it from a human.

contentFile !-

The bytes to push: one file, one layer. It is read into memory whole, as Attach and PushArtifact read theirs, so this is for documents rather than for image layers.

mediaTypeString !-

The layer’s media type, which is what a consumer filters layers on.

annotationsString -

Annotations to set on the layer, as a JSON object whose values are strings. Empty sets none. It is JSON rather than a map because codegen has no map type.

Example
dagger -m github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb call \
 registry --host string \
 push-layer --repository string --tag string --content file:path --media-type string
func (m *MyModule) Example(ctx context.Context, host string, repository string, tag string, content *dagger.File, mediaType string) string  {
	return dag.
			Oci().
			Registry(host).
			Pushlayer(ctx, repository, tag, content, mediaType)
}
@function
async def example(host: str, repository: str, tag: str, content: dagger.File, mediatype: str) -> str:
	return await (
		dag.oci()
		.registry(host)
		.pushlayer(repository, tag, content, mediatype)
	)
@func()
async example(host: string, repository: string, tag: string, content: File, mediaType: string): Promise<string> {
	return dag
		.oci()
		.registry(host)
		.pushLayer(repository, tag, content, mediaType)
}

referrers() 🔗

Referrers lists the artifacts attached to subject as a JSON array of OCI descriptors, newest registry ordering preserved.

It returns JSON rather than a typed object for two reasons: codegen has no map type, so annotations could not be modelled; and a module object returned from a never-cached call detaches in Dagger v0.21, so lazily reading its fields fails.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
repositoryString !-

Repository holding the subject.

subjectString !-

Digest of the manifest whose referrers are wanted.

artifactTypeString -

Restrict the listing to one artifact type. Empty lists them all.

Example
dagger -m github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb call \
 registry --host string \
 referrers --repository string --subject string
func (m *MyModule) Example(ctx context.Context, host string, repository string, subject string) string  {
	return dag.
			Oci().
			Registry(host).
			Referrers(ctx, repository, subject)
}
@function
async def example(host: str, repository: str, subject: str) -> str:
	return await (
		dag.oci()
		.registry(host)
		.referrers(repository, subject)
	)
@func()
async example(host: string, repository: string, subject: string): Promise<string> {
	return dag
		.oci()
		.registry(host)
		.referrers(repository, subject)
}

resolve() 🔗

Resolve returns the digest a tag currently points at.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
repositoryString !-

Repository holding the tag.

tagString !-

Tag to resolve.

Example
dagger -m github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb call \
 registry --host string \
 resolve --repository string --tag string
func (m *MyModule) Example(ctx context.Context, host string, repository string, tag string) string  {
	return dag.
			Oci().
			Registry(host).
			Resolve(ctx, repository, tag)
}
@function
async def example(host: str, repository: str, tag: str) -> str:
	return await (
		dag.oci()
		.registry(host)
		.resolve(repository, tag)
	)
@func()
async example(host: string, repository: string, tag: string): Promise<string> {
	return dag
		.oci()
		.registry(host)
		.resolve(repository, tag)
}

tag() 🔗

Tag points tag at a manifest already in repository, named by its digest, and returns the digest it now resolves to.

It moves an existing tag as readily as it creates a new one — a tag is a mutable name, and a registry PUT of a manifest under a tag is the only operation either case has. What it will not do is invent the bytes: the digest is read from the registry first, so tagging something that is not there fails naming the digest instead of leaving a tag that resolves to nothing.

Nothing is re-uploaded. The manifest is fetched and PUT back under the new name, which is bytes the registry already holds; the blobs it names are untouched.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
repositoryString !-

Repository holding the manifest.

digestString !-

Digest of the manifest to name, e.g. “sha256:…”.

tagString !-

Tag to point at it.

Example
dagger -m github.com/z5labs/devex/daggerverse/oci@c10a12007999eb807f68cd9af9000fbaeab159cb call \
 registry --host string \
 tag --repository string --digest string --tag string
func (m *MyModule) Example(ctx context.Context, host string, repository string, digest string, tag string) string  {
	return dag.
			Oci().
			Registry(host).
			Tag(ctx, repository, digest, tag)
}
@function
async def example(host: str, repository: str, digest: str, tag: str) -> str:
	return await (
		dag.oci()
		.registry(host)
		.tag(repository, digest, tag)
	)
@func()
async example(host: string, repository: string, digest: string, tag: string): Promise<string> {
	return dag
		.oci()
		.registry(host)
		.tag(repository, digest, tag)
}