Share and reuse Starlark modules across compositions by publishing them to any OCI-compatible container registry (GHCR, ACR, ECR, Docker Hub, Harbor, etc.).
function-starlark resolves OCI modules before any Starlark code runs:
- Scan the main script and inline modules for OCI load targets (both
short-form
package:tag/file.starand explicitoci://URLs) - Expand short-form targets using the configured default registry
- Deduplicate references (same registry/repo:tag = one pull)
- Fetch artifacts from the registry (or serve from in-memory cache)
- Scan fetched modules for transitive OCI loads, repeat until resolved
- Inject all resolved
.starfiles into the inline module map - Execute the script -- all OCI modules available as if they were inline
This resolve-then-execute architecture preserves Starlark's sandbox hermeticity: no network access happens during script execution.
When a default OCI registry is configured (see Configuring the Default Registry), use the concise short-form syntax:
# Load by tag
load("my-org-starlark-lib:v1/helpers.star", "create_bucket", "create_topic")
# Load by digest (deterministic, skips tag resolution)
load("my-org-starlark-lib@sha256:abc123.../helpers.star", "create_bucket")
# Star import -- all public exports
load("my-org-starlark-lib:v1/helpers.star", "*")Short-form references are expanded using the default registry. For example,
with registry ghcr.io/my-org, my-org-starlark-lib:v1/helpers.star becomes
oci://ghcr.io/my-org/my-org-starlark-lib:v1/helpers.star.
Inside a module that was pulled from an OCI artifact, use ./sibling.star to
load another file in the same artifact (same registry, repo, and tag or
digest). Package-local loads are the recommended way for a published package
to reference its own siblings.
# main.star — published inside ghcr.io/my-org/platform-lib:v1 alongside
# helper.star and values.star.
load("./helper.star", "helper")
load("./values.star", "greeting")
message = greeting + ", " + helperKey properties:
-
Resolves to the same
registry/repo:tag-or-digestthe caller was pulled from — no second fetch, noociDefaultRegistryrequired for the sibling. -
Valid only from OCI callers. Using
./sibling.starfrom a main composition, inline module, or filesystem module produces:package-local load "./sibling.star" is only valid from OCI modules; caller "composition.star" is not an OCI module -
Flat paths only:
./foo.staris accepted;./sub/foo.star,../foo.star, and./are rejected at scan time. -
Works with both tag-pinned and digest-pinned callers — the sibling inherits whichever reference form the caller arrived with.
Use package-local loads in published packages instead of short-form cross-
package references: consumers no longer need to configure ociDefaultRegistry
to resolve your internal dependencies, and the artifact is pulled once.
Use the oci:// prefix for full control over the registry path, or when no
default registry is configured:
# Load by tag
load("oci://ghcr.io/my-org/starlark-lib:v1/helpers.star", "create_bucket", "create_topic")
# Load by digest (deterministic, skips tag resolution)
load("oci://ghcr.io/my-org/starlark-lib@sha256:abc123.../helpers.star", "create_bucket")
# Star import -- all public exports
load("oci://ghcr.io/my-org/starlark-lib:v1/helpers.star", "*")oci://registry/repo[:tag|@sha256:digest]/file.star
| Component | Required | Example |
|---|---|---|
oci:// prefix |
Yes | oci:// |
| Registry | Yes | ghcr.io, myregistry.azurecr.io, localhost:5000 |
| Repository | Yes | my-org/starlark-lib, modules/networking |
| Tag or digest | Yes | :v1, @sha256:abcdef... |
| File path | Yes | /helpers.star, /networking.star |
Implicit :latest is not supported — always specify an explicit tag or
digest. This is intentional: compositions should be reproducible.
load("module.star", "*") imports all non-underscore exports from a module.
This works for all module types — inline, filesystem, and OCI:
# Star import: brings in everything the module exports
load("oci://ghcr.io/my-org/lib:v1/helpers.star", "*")
# Equivalent to listing every export by name:
# load("oci://ghcr.io/my-org/lib:v1/helpers.star", "create_bucket", "create_topic", "tag_resource")
# Mix named and star imports:
load("oci://ghcr.io/my-org/lib:v1/helpers.star", "create_bucket", "*")Names starting with _ are private and never exported through star import.
Namespace aliases solve name conflicts when loading from multiple OCI packages
that export the same type names. Use the alias="*" syntax to wrap all exports
in a struct:
# Short-form with namespace alias
load("schemas-k8s:v1.35/apps/v1.star", k8s="*")
load("schemas-azure:v2.5.0/storage/v1.star", storage="*")
# Explicit URL with namespace alias
load("oci://ghcr.io/wompipomp/schemas-azure:v2.5.0/cosmosdb/v1.star", cosmosdb="*")
# Access via dot notation
k8s.Deployment(...)
storage.Account(...)
cosmosdb.Account(...)Namespace aliases work identically for short-form and explicit OCI loads. See the module system guide for full syntax details and mixed import examples.
The default registry enables short-form load syntax by providing the
registry/namespace prefix for expansion. Configure it at the operator level
(all compositions) or per-composition.
Set STARLARK_OCI_DEFAULT_REGISTRY on the function pod via a
DeploymentRuntimeConfig:
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
name: function-starlark
spec:
deploymentTemplate:
spec:
template:
spec:
containers:
- name: package-runtime
env:
- name: STARLARK_OCI_DEFAULT_REGISTRY
value: "ghcr.io/my-org"
# If you also need private registry auth, add volume mounts here
volumeMounts:
- name: registry-creds
mountPath: /var/run/secrets/docker/my-registry-creds
readOnly: true
volumes:
- name: registry-creds
secret:
secretName: my-registry-creds
items:
- key: .dockerconfigjson
path: config.jsonReference the runtime config in your Function:
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
name: function-starlark
spec:
package: ghcr.io/wompipomp/function-starlark:latest
runtimeConfigRef:
name: function-starlarkSet spec.ociDefaultRegistry in the StarlarkInput to override or replace the
environment variable for a specific composition:
apiVersion: starlark.fn.crossplane.io/v1alpha1
kind: StarlarkInput
spec:
ociDefaultRegistry: "ghcr.io/my-org"
source: |
load("my-starlark-lib:v1/helpers.star", "create_bucket")
Resource("bucket", create_bucket("us-east-1"))spec.ociDefaultRegistry (non-empty) takes precedence over the
STARLARK_OCI_DEFAULT_REGISTRY environment variable. If neither is configured
and a short-form load target is encountered, the function returns a fatal error
with a clear message explaining both configuration options.
If you use the function-starlark VS Code extension for schema IntelliSense, you also need to configure the default registry in VS Code settings so the editor can resolve short-form load targets:
{
"functionStarlark.schemas.registry": "ghcr.io/my-org"
}The default is ghcr.io/wompipomp. Keep this in sync with your runtime
registry configuration so that editor diagnostics match deployed behavior.
The registry value is host/namespace (e.g., ghcr.io/my-org). Do not
include the oci:// prefix -- it is stripped silently if present. Trailing
slashes are also stripped silently.
Modules are published as OCI artifacts using oras. Each
artifact is a flat tar of .star files with a custom media type.
# macOS
brew install oras
# Linux
curl -LO https://github.com/oras-project/oras/releases/download/v1.2.2/oras_1.2.2_linux_amd64.tar.gz
tar xzf oras_1.2.2_linux_amd64.tar.gz
sudo mv oras /usr/local/bin/# Single file
oras push ghcr.io/my-org/starlark-lib:v1 \
--artifact-type application/vnd.fn-starlark.modules.v1+tar \
helpers.star
# Multiple files in one bundle
oras push ghcr.io/my-org/starlark-lib:v1 \
--artifact-type application/vnd.fn-starlark.modules.v1+tar \
helpers.star naming.star networking.star
# With digest pinning output
oras push ghcr.io/my-org/starlark-lib:v1 \
--artifact-type application/vnd.fn-starlark.modules.v1+tar \
helpers.star 2>&1 | grep Digest
# Digest: sha256:abc123...| Type | Value |
|---|---|
| Artifact (config) | application/vnd.fn-starlark.modules.v1+tar |
| Layer | application/vnd.fn-starlark.layer.v1.tar |
function-starlark validates both media types on pull and rejects artifacts that don't match. This prevents accidentally loading non-Starlark OCI artifacts.
The tar layer must contain .star files at the root — no directories, no
nested paths. Safety limits enforced on extraction:
- Files must end in
.star(non-star files are silently skipped) - Maximum 100 files per bundle
- Maximum 1 MB per file
- No path traversal (
.., absolute paths)
Use semantic version tags for your module bundles:
# Development
oras push ghcr.io/my-org/starlark-lib:v1.2.0-dev helpers.star
# Release
oras push ghcr.io/my-org/starlark-lib:v1.2.0 helpers.star
# Major version alias (pin compositions to :v1 for compatible updates)
oras tag ghcr.io/my-org/starlark-lib:v1.2.0 v1No configuration needed. Anonymous pulls work for public repositories.
Two steps: tell function-starlark which secret to use, and mount the secret into the function pod.
1. Set dockerConfigSecret in your Composition:
apiVersion: starlark.fn.crossplane.io/v1alpha1
kind: StarlarkInput
spec:
dockerConfigSecret: my-registry-creds
source: |
load("oci://myregistry.azurecr.io/modules/helpers:v1/helpers.star", "create_bucket")
Resource("bucket", create_bucket("us-east-1"))2. Create the Kubernetes Secret:
# From existing Docker config
kubectl create secret docker-registry my-registry-creds \
--docker-server=myregistry.azurecr.io \
--docker-username=<username> \
--docker-password=<password> \
-n crossplane-system
# Or from an existing .dockerconfigjson
kubectl create secret generic my-registry-creds \
--from-file=.dockerconfigjson=$HOME/.docker/config.json \
--type=kubernetes.io/dockerconfigjson \
-n crossplane-system3. Mount the secret via DeploymentRuntimeConfig:
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
name: function-starlark
spec:
deploymentTemplate:
spec:
template:
spec:
containers:
- name: package-runtime
volumeMounts:
- name: registry-creds
mountPath: /var/run/secrets/docker/my-registry-creds
readOnly: true
volumes:
- name: registry-creds
secret:
secretName: my-registry-creds
items:
- key: .dockerconfigjson
path: config.jsonThe items mapping renames .dockerconfigjson to config.json because
go-containerregistry's credential chain expects standard Docker config format.
4. Reference the runtime config in your Function:
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
name: function-starlark
spec:
package: ghcr.io/my-org/function-starlark:v0.1.0
runtimeConfigRef:
name: function-starlark# Create secret from ACR admin credentials
kubectl create secret docker-registry acr-creds \
--docker-server=myregistry.azurecr.io \
--docker-username=$(az acr credential show -n myregistry --query username -o tsv) \
--docker-password=$(az acr credential show -n myregistry --query passwords[0].value -o tsv) \
-n crossplane-system
# Or use a service principal for non-interactive auth
kubectl create secret docker-registry acr-creds \
--docker-server=myregistry.azurecr.io \
--docker-username=<sp-app-id> \
--docker-password=<sp-password> \
-n crossplane-system# ECR token (expires every 12 hours — use a CronJob or external-secrets to refresh)
TOKEN=$(aws ecr get-login-password --region us-east-1)
kubectl create secret docker-registry ecr-creds \
--docker-server=123456789.dkr.ecr.us-east-1.amazonaws.com \
--docker-username=AWS \
--docker-password=$TOKEN \
-n crossplane-systemkubectl create secret docker-registry ghcr-creds \
--docker-server=ghcr.io \
--docker-username=<github-username> \
--docker-password=<github-pat> \
-n crossplane-systemcrossplane render cannot mount volumes into function containers, so the
in-cluster dockerConfigSecret + DeploymentRuntimeConfig approach does not work
locally. Instead, use dockerConfigCredential to pass Docker credentials via
the gRPC request.
crossplane render --function-credentials <file> reads a Kubernetes Secret
manifest from the file and delivers its data to the function via gRPC. The
function extracts the Docker config.json (or .dockerconfigjson) from the
credential data and uses it to authenticate against private registries.
Both mechanisms can coexist in the same Composition:
| Mechanism | Field | When used |
|---|---|---|
| gRPC credential | dockerConfigCredential |
crossplane render locally + in-cluster via Composition credentials block |
| Filesystem mount | dockerConfigSecret |
In-cluster via DeploymentRuntimeConfig (set via env var STARLARK_DOCKER_CONFIG_SECRET) |
The keychain priority is: gRPC credential → filesystem secret → host default.
Important: If Docker Desktop is installed,
~/.docker/config.jsonlikely contains"credsStore": "desktop"instead of actual credentials. This means the real tokens are in Docker Desktop's keychain, not in the file — and they won't be available tocrossplane render. You must generate a credentials file with inline credentials instead.
Azure Container Registry:
# Get an ACR access token and build a config.json with inline credentials
TOKEN=$(az acr login --name myregistry --expose-token --output tsv --query accessToken)
AUTH=$(printf '00000000-0000-0000-0000-000000000000:%s' "$TOKEN" | base64)
printf '{"auths":{"myregistry.azurecr.io":{"auth":"%s"}}}' "$AUTH" > /tmp/config.json
kubectl create secret generic docker-config \
--from-file=config.json=/tmp/config.json \
--namespace=crossplane-system \
--dry-run=client -o yaml > credentials.yaml
rm /tmp/config.jsonThe ACR token expires after ~3 hours. Re-run the commands to refresh.
GitHub Container Registry (GHCR):
# Create a PAT at GitHub → Settings → Developer settings → Personal access tokens
# with read:packages scope. The PAT is used directly — no token exchange needed.
AUTH=$(printf '<github-username>:<github-pat>' | base64)
printf '{"auths":{"ghcr.io":{"auth":"%s"}}}' "$AUTH" > /tmp/config.json
kubectl create secret generic docker-config \
--from-file=config.json=/tmp/config.json \
--namespace=crossplane-system \
--dry-run=client -o yaml > credentials.yaml
rm /tmp/config.jsonGitLab Container Registry:
# Create a PAT at GitLab → Settings → Access Tokens with read_registry scope,
# or use a deploy token (username is the deploy token name, not your GitLab username).
AUTH=$(printf '<username>:<token>' | base64)
printf '{"auths":{"registry.gitlab.com":{"auth":"%s"}}}' "$AUTH" > /tmp/config.json
kubectl create secret generic docker-config \
--from-file=config.json=/tmp/config.json \
--namespace=crossplane-system \
--dry-run=client -o yaml > credentials.yaml
rm /tmp/config.jsonAmazon ECR:
# ECR tokens expire every 12 hours — re-run to refresh.
TOKEN=$(aws ecr get-login-password --region us-east-1)
AUTH=$(printf 'AWS:%s' "$TOKEN" | base64)
printf '{"auths":{"123456789.dkr.ecr.us-east-1.amazonaws.com":{"auth":"%s"}}}' "$AUTH" > /tmp/config.json
kubectl create secret generic docker-config \
--from-file=config.json=/tmp/config.json \
--namespace=crossplane-system \
--dry-run=client -o yaml > credentials.yaml
rm /tmp/config.jsonMultiple registries in one file:
# Combine multiple registries into a single credentials file
printf '{"auths":{"ghcr.io":{"auth":"%s"},"registry.gitlab.com":{"auth":"%s"}}}' \
"$GHCR_AUTH" "$GITLAB_AUTH" > /tmp/config.jsonOther registries / no credsStore:
# If your ~/.docker/config.json already contains inline credentials:
kubectl create secret generic docker-config \
--from-file=config.json=$HOME/.docker/config.json \
--namespace=crossplane-system \
--dry-run=client -o yaml > credentials.yamlpipeline:
- step: starlark
functionRef:
name: function-starlark
credentials:
- name: registry-creds
source: Secret
secretRef:
name: docker-config
namespace: crossplane-system
input:
apiVersion: starlark.fn.crossplane.io/v1alpha1
kind: StarlarkInput
spec:
dockerConfigCredential: registry-creds
source: |
load("oci://myregistry.azurecr.io/modules/helpers:v1/helpers.star", "*")crossplane render xr.yaml composition.yaml functions.yaml \
--function-credentials credentials.yamlcrossplane render matches credentials by both name and namespace. The
secretRef in the Composition must exactly match the metadata in your
credentials file:
# credentials.yaml # Composition secretRef
metadata: secretRef:
name: docker-config ← must → name: docker-config
namespace: crossplane-system ← match → namespace: crossplane-systemIf they don't match, the credential won't be found and the function silently falls back to the default keychain (which will fail for private registries).
If your in-cluster setup uses dockerConfigSecret via DeploymentRuntimeConfig
and environment variable, you can add dockerConfigCredential to the same
Composition without conflict. The gRPC credential is only used when present in
the request — during crossplane render or when the Composition has a
credentials block pointing to a valid Secret. In-cluster, the filesystem
secret still works independently via the env var.
OCI modules are cached in-memory with a two-layer architecture:
| Layer | Key | Lifetime | Purpose |
|---|---|---|---|
| Tag cache | registry/repo:tag → digest |
Governed by STARLARK_OCI_PULL_POLICY |
Avoid revalidating tags on every reconciliation |
| Content cache | sha256:... → .star files |
Pod lifetime (immutable) | Content-addressed; same digest = same content |
Revalidation follows a Kubernetes-style pull policy:
STARLARK_OCI_PULL_POLICY |
Behavior |
|---|---|
IfNotPresent (default) |
First reference pulls the artifact; every later reconciliation reuses the cached copy for the pod's lifetime. No HEAD checks, zero steady-state registry traffic. Refresh by restarting the pod or pointing at a different tag/digest. |
Always |
On cache miss or after STARLARK_OCI_CACHE_TTL expires, the resolver issues one manifest HEAD. Unchanged digest → serve cached content (no blob transfer). Changed digest → pull the new manifest and layers. |
STARLARK_OCI_CACHE_TTL is only consulted under Always; it has no effect
under IfNotPresent. 0 means "revalidate on every reconciliation."
Configure on the function pod via DeploymentRuntimeConfig:
env:
- name: STARLARK_OCI_PULL_POLICY
value: "Always" # opt-in if you retag and want in-place updates
- name: STARLARK_OCI_CACHE_TTL
value: "10m" # only applies when policy is Always- Hit under
IfNotPresent→ serve from cache, zero network calls. - Fresh hit under
Always(within TTL) → serve from cache, zero network calls. - Stale under
Always(TTL expired, registry reachable) → HEAD the tag. If the digest matches, refresh TTL and serve cached content. If it changed, pull the new artifact. - Stale + registry down → serve last-known-good content with a warning.
- Cold miss + registry down → fail fast with an error naming the unreachable registry.
The cache lives in-memory on the function pod. It does not survive pod restarts — the first reconciliation after a restart pays the OCI pull cost (~200-500ms), then all subsequent reconciliations serve from memory.
load("oci://ghcr.io/my-org/lib@sha256:abc123.../helpers.star", "create_bucket")Digest references go directly to the content cache layer, unaffected by
STARLARK_OCI_PULL_POLICY. If the digest is cached, it's served immediately.
If not, it's pulled once and cached forever. This is the most deterministic
option for production compositions.
OCI modules can load other OCI modules. function-starlark resolves the full dependency tree before execution:
# helpers.star (published to ghcr.io/my-org/lib:v1)
load("oci://ghcr.io/my-org/base:v1/naming.star", "resource_name")
def create_bucket(region):
return {"apiVersion": "s3.aws.upbound.io/v1beta1", "kind": "Bucket",
"metadata": {"name": resource_name("bucket", region)}}# composition.star (your composition)
load("oci://ghcr.io/my-org/lib:v1/helpers.star", "create_bucket")
Resource("bucket", create_bucket("us-east-1"))function-starlark will:
- Scan
composition.star→ findghcr.io/my-org/lib:v1 - Pull and extract
helpers.star - Scan
helpers.star→ findghcr.io/my-org/base:v1 - Pull and extract
naming.star - Inject both into the inline module map
- Execute — all transitive deps available
Circular dependencies are detected and produce a clear error. If module A loads module B which loads module A, resolution fails before execution.
Published packages should use package-local loads
(./sibling.star) for references inside the same artifact, rather than a
short-form cross-package reference like my-lib:v1/other.star.
Why:
- No consumer configuration. A consumer can load your package without
setting
ociDefaultRegistryjust for your internal helpers. - No double-pull. Short-form intra-package refs go through the transitive resolver and can pull the package again (sometimes a different version if the consumer's default registry resolves to a different org), wasting time and potentially mixing versions. Package-local refs resolve inside the already-pulled artifact.
- Version coherence. A sibling referenced via
./always matches the caller's tag or digest, so there's no risk of a module calling a stale copy of its own package.
Before (drags in a second copy of the package through the default registry):
# platform.star — published as ghcr.io/my-org/lib:v1
load("lib:v1/naming.star", "resource_name") # short-form cross-packageAfter (resolves inside the same artifact, no registry config needed):
# platform.star — published as ghcr.io/my-org/lib:v1
load("./naming.star", "resource_name") # package-local# helpers.star — published to ghcr.io/acme/platform-lib:v1
def s3_bucket(name, region, tags={}):
"""Create a standard S3 bucket with org defaults."""
return {
"apiVersion": "s3.aws.upbound.io/v1beta1",
"kind": "Bucket",
"metadata": {"name": name, "labels": {"team": "platform"}},
"spec": {"forProvider": {"region": region, "tags": tags}},
}
def rds_instance(name, region, engine="postgres", size="db.t3.micro"):
"""Create a standard RDS instance."""
return {
"apiVersion": "rds.aws.upbound.io/v1beta1",
"kind": "Instance",
"metadata": {"name": name},
"spec": {"forProvider": {
"region": region, "engine": engine, "instanceClass": size,
}},
}oras push ghcr.io/acme/platform-lib:v1 \
--artifact-type application/vnd.fn-starlark.modules.v1+tar \
helpers.starapiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xdatabases.acme.io
spec:
compositeTypeRef:
apiVersion: acme.io/v1
kind: XDatabase
mode: Pipeline
pipeline:
- step: create-resources
functionRef:
name: function-starlark
input:
apiVersion: starlark.fn.crossplane.io/v1alpha1
kind: StarlarkInput
spec:
dockerConfigSecret: ghcr-creds
source: |
load("oci://ghcr.io/acme/platform-lib:v1/helpers.star", "*")
region = get(oxr, "spec.region", "us-east-1")
name = get(oxr, "metadata.name", "db")
Resource("bucket", s3_bucket(name + "-backups", region))
Resource("database", rds_instance(name, region,
engine=get(oxr, "spec.engine", "postgres"),
size=get(oxr, "spec.size", "db.t3.micro")))function-starlark ships a standard library of Crossplane helpers published to
ghcr.io/wompipomp/starlark-stdlib. It provides four modules covering the
most common composition patterns:
| Module | Purpose |
|---|---|
networking.star |
CIDR math, IP address utilities (equivalent to Terraform's cidrsubnet) |
naming.star |
Kubernetes-safe resource naming with 63-character limit enforcement |
labels.star |
Kubernetes recommended labels and Crossplane labels with merge utility |
conditions.star |
Operational status signaling (degraded) |
# Short-form (recommended, requires default registry ghcr.io/wompipomp)
load("starlark-stdlib:v1/networking.star", "subnet_cidr", "cidr_contains")
load("starlark-stdlib:v1/naming.star", "resource_name")
load("starlark-stdlib:v1/labels.star", "standard_labels", "crossplane_labels", "merge_labels")
load("starlark-stdlib:v1/conditions.star", "degraded")
# Or use star import to get everything from a module
load("starlark-stdlib:v1/networking.star", "*")
# Explicit full URL (always works, no default registry needed)
load("oci://ghcr.io/wompipomp/starlark-stdlib:v1/networking.star", "subnet_cidr")# With default registry configured to ghcr.io/wompipomp
load("starlark-stdlib:v1/naming.star", "resource_name")
load("starlark-stdlib:v1/labels.star", "standard_labels", "crossplane_labels", "merge_labels")
name = resource_name("bucket")
labels = merge_labels(
standard_labels("my-app", component="storage"),
crossplane_labels(),
)
Resource("bucket", {
"apiVersion": "s3.aws.upbound.io/v1beta1",
"kind": "Bucket",
"metadata": {"name": name, "labels": labels},
"spec": {"forProvider": {"region": get(oxr, "spec.region", "us-east-1")}},
})The stdlib is a public GHCR package -- no authentication is needed to pull it. For full API documentation, see Standard Library Reference.
The following fields are relevant to OCI module distribution:
spec:
# Default OCI registry for short-form load syntax (overrides env var)
# Format: "registry/namespace" (e.g. "ghcr.io/my-org")
ociDefaultRegistry: "ghcr.io/my-org"
# Registries to access over plain HTTP (overrides env var)
ociInsecureRegistries: ["localhost:5050"]
# Name of the Kubernetes Secret with Docker registry credentials
# Must be mounted via DeploymentRuntimeConfig (overrides env var)
dockerConfigSecret: "my-registry-creds"
# Inline modules (OCI-resolved modules are merged into this map)
modules:
local-helpers.star: |
def local_fn(): return "local"OCI load target "oci://ghcr.io/my-org/lib/helpers.star": tag or digest required
Add an explicit tag or digest. Implicit :latest is not supported:
# Bad
load("oci://ghcr.io/my-org/lib/helpers.star", "fn")
# Good
load("oci://ghcr.io/my-org/lib:v1/helpers.star", "fn")unexpected artifact media type "application/vnd.oci.image.config.v1+json" for ghcr.io/my-org/lib:v1
The artifact was pushed without the correct --artifact-type. Re-push with:
oras push ghcr.io/my-org/lib:v1 \
--artifact-type application/vnd.fn-starlark.modules.v1+tar \
helpers.starOCI module "helpers.star" not resolved; ensure the OCI reference was resolvable
The OCI scanner didn't find the oci:// load target in the script (parse error
in the source), or the registry was unreachable with a cold cache. Check:
- The
load()statement has correctoci://syntax - The registry is reachable from the function pod
- Credentials are mounted if the registry is private
# Check the function pod logs
kubectl logs -n crossplane-system -l pkg.crossplane.io/function=function-starlark
# Verify the secret exists and is correctly mounted
kubectl get secret my-registry-creds -n crossplane-system -o jsonpath='{.type}'
# Should be: kubernetes.io/dockerconfigjson
# Verify the DeploymentRuntimeConfig mount
kubectl get pods -n crossplane-system -l pkg.crossplane.io/function=function-starlark \
-o jsonpath='{.items[0].spec.containers[0].volumeMounts}' | jq .