Skip to content

Latest commit

 

History

History
941 lines (723 loc) · 30.4 KB

File metadata and controls

941 lines (723 loc) · 30.4 KB

OCI Module Distribution

Share and reuse Starlark modules across compositions by publishing them to any OCI-compatible container registry (GHCR, ACR, ECR, Docker Hub, Harbor, etc.).

How it works

function-starlark resolves OCI modules before any Starlark code runs:

  1. Scan the main script and inline modules for OCI load targets (both short-form package:tag/file.star and explicit oci:// URLs)
  2. Expand short-form targets using the configured default registry
  3. Deduplicate references (same registry/repo:tag = one pull)
  4. Fetch artifacts from the registry (or serve from in-memory cache)
  5. Scan fetched modules for transitive OCI loads, repeat until resolved
  6. Inject all resolved .star files into the inline module map
  7. 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.

Loading OCI modules

Short-form (recommended)

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.

Package-local loads

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 + ", " + helper

Key properties:

  • Resolves to the same registry/repo:tag-or-digest the caller was pulled from — no second fetch, no ociDefaultRegistry required for the sibling.

  • Valid only from OCI callers. Using ./sibling.star from 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.star is 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.

Explicit full URL

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", "*")

URL format

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.

Star import

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 alias imports

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.

Configuring the Default Registry

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.

Environment variable (operator-level)

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.json

Reference 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-starlark

Spec field (per-composition override)

Set 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"))

Precedence

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.

Editor configuration

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.

Registry value format

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.

Publishing modules

Modules are published as OCI artifacts using oras. Each artifact is a flat tar of .star files with a custom media type.

Install oras

# 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/

Push a module bundle

# 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...

Media types

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.

Bundle layout

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)

Versioning strategy

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 v1

Authentication

Public registries

No configuration needed. Anonymous pulls work for public repositories.

Private registries (ACR, ECR, GHCR, etc.)

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-system

3. 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.json

The 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

Azure Container Registry (ACR)

# 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

Amazon ECR

# 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-system

GitHub Container Registry (GHCR)

kubectl create secret docker-registry ghcr-creds \
  --docker-server=ghcr.io \
  --docker-username=<github-username> \
  --docker-password=<github-pat> \
  -n crossplane-system

Local development with crossplane render

crossplane 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.

How it works

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.

Step 1: Generate a credentials file

Important: If Docker Desktop is installed, ~/.docker/config.json likely 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 to crossplane 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.json

The 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.json

GitLab 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.json

Amazon 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.json

Multiple 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.json

Other 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.yaml

Step 2: Add dockerConfigCredential to your Composition

pipeline:
- 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", "*")

Step 3: Render

crossplane render xr.yaml composition.yaml functions.yaml \
  --function-credentials credentials.yaml

Namespace matching

crossplane 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-system

If 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).

Coexisting with in-cluster dockerConfigSecret

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.

Caching

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

Pull policy

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

How cache lookups work

  1. Hit under IfNotPresent → serve from cache, zero network calls.
  2. Fresh hit under Always (within TTL) → serve from cache, zero network calls.
  3. 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.
  4. Stale + registry down → serve last-known-good content with a warning.
  5. 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.

Digest-pinned references skip the tag cache

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.

Transitive dependencies

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:

  1. Scan composition.star → find ghcr.io/my-org/lib:v1
  2. Pull and extract helpers.star
  3. Scan helpers.star → find ghcr.io/my-org/base:v1
  4. Pull and extract naming.star
  5. Inject both into the inline module map
  6. 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.

Intra-package references: prefer ./sibling.star

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 ociDefaultRegistry just 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-package

After (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

Complete example

Module library

# 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,
        }},
    }

Publish

oras push ghcr.io/acme/platform-lib:v1 \
  --artifact-type application/vnd.fn-starlark.modules.v1+tar \
  helpers.star

Composition

apiVersion: 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")))

Using the standard library

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)

Loading stdlib modules

# 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")

Example composition using stdlib

# 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.

StarlarkInput 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"

Troubleshooting

"tag or digest required"

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")

"artifact media type mismatch"

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.star

"OCI module not resolved"

OCI 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:

  1. The load() statement has correct oci:// syntax
  2. The registry is reachable from the function pod
  3. Credentials are mounted if the registry is private

Registry authentication failures

# 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 .