Skip to content

frostmoln_container_registry_cache (Resource) ​

A pull-through cache in this tenant's container registry — a namespace that mirrors ONE public registry on demand. One upstream per cache: a tenant wanting three upstreams creates three of these.

Pull through it with the SAME credential that reaches your repository namespace — docker login is per host, so one credential spans both, and existing credentials are extended onto a new cache automatically.

CACHED BYTES COUNT AGAINST YOUR STORAGE ALLOWANCE. A cache is your storage: one quota, one meter, shared with your repository namespace. It fills on PULL, so it can consume the allowance with no push from you.

THERE IS NO UPDATE ROUTE. Every argument here is replace-only, including password: changing credentials means destroying the cache and creating it again. DESTROYING A CACHE DELETES ITS CACHED IMAGES, and does not ask — every byte in it is a copy of something still at the upstream, so a later pull simply re-fetches, but your stored-bytes figure falls as the content goes.

The registry must already exist: a cache cannot be created before the tenant has opted in with a frostmoln_container_registry resource.

Authoritative scope — who owns what on this resource, declared in internal/scopedecl and machine-checked against the schema.

Enacted and reconciled — every configurable attribute not listed below: the platform applies it, and a refresh reads the truth back.

Create-immutable — password, password_wo_version, upstream, username: the cache's upstream and its credentials are fixed at create — a change re-creates the cache.

Not enacted state — password_wo: write-only: sent, never stored or read back — the password_wo_version companion carries change detection.

Example Usage ​

terraform
resource "frostmoln_container_registry" "example" {}

# The upstream catalog is SERVER-OWNED. Read it rather than writing a literal
# list of keys: rows are added and retired without a provider release, so a key
# copied out of documentation can be one the server no longer offers.
data "frostmoln_container_registry_cache_upstreams" "available" {
  depends_on = [frostmoln_container_registry.example]
}

# A cache for an upstream that needs no credentials of your own.
resource "frostmoln_container_registry_cache" "ghcr" {
  upstream = "ghcr"

  # A cache cannot be created before the registry exists.
  depends_on = [frostmoln_container_registry.example]
}

# Docker Hub demands YOUR OWN credentials: an anonymous mirror of it draws on an
# allowance shared with other customers, so the server refuses one. Which
# upstreams demand them is the `requires_credentials` flag on the catalog row —
# read the flag, do not special-case the key.
#
# These are WRITE-ONLY. No endpoint returns them, so Terraform reports whatever
# it last wrote and cannot detect a change made elsewhere. There is no update
# route either: changing the password REPLACES the cache and deletes what it had
# cached.
resource "frostmoln_container_registry_cache" "dockerhub" {
  upstream = "dockerhub"
  username = var.dockerhub_username

  # password_wo is never written to the plan or to Terraform state (Terraform
  # 1.11+). Because Terraform cannot see it, it cannot detect a change to it:
  # password_wo_version is the ONLY signal that makes a later apply send the
  # current value — and it does so by REPLACING the cache, deleting what it had
  # cached. Editing the token without bumping the version does nothing.
  #
  # `password` is the older spelling, mutually exclusive with this one; it works
  # on any Terraform version but is stored in state in plaintext.
  password_wo         = var.dockerhub_token
  password_wo_version = "1"

  depends_on = [frostmoln_container_registry.example]
}

variable "dockerhub_username" {
  type = string
}

variable "dockerhub_token" {
  type      = string
  sensitive = true
}

# Driving caches from the catalog itself, so the configuration cannot name a key
# the server has retired. Only the upstreams needing no credentials are taken
# here; one that requires them needs its own resource with its own secrets.
resource "frostmoln_container_registry_cache" "anonymous" {
  for_each = {
    for u in data.frostmoln_container_registry_cache_upstreams.available.upstreams :
    u.key => u if !u.requires_credentials
  }

  upstream = each.key
}

# What to prefix an image with to pull it through the cache. Pull with the SAME
# credential that reaches your repository namespace — `docker login` is per
# host, so one credential spans both, and existing credentials are extended onto
# a new cache automatically.
output "cached_image_examples" {
  description = "How to pull through each cache."
  value = {
    for k, c in frostmoln_container_registry_cache.anonymous :
    k => "${c.pull_path}/<image>:<tag>"
  }
}

# CACHED BYTES COUNT AGAINST YOUR STORAGE ALLOWANCE: one quota, one meter,
# shared with your repository namespace. A cache fills on PULL, so it can
# consume the allowance with no push from you. The aggregate figure is on the
# registry itself — there is deliberately no per-cache number to add up.
output "registry_storage_used_bytes" {
  value = frostmoln_container_registry.example.storage_used_bytes
}

Schema ​

Required ​

  • upstream (String) The upstream to mirror, as a KEY from the server's catalog — never a URL, which is not accepted in any form and has no field. Read the available keys from the frostmoln_container_registry_cache_upstreams data source rather than from any published list: the catalog is server-owned, rows are added and retired, and a key copied into a configuration from documentation can be one the server no longer offers. Changing it replaces the cache and deletes what it had cached.

Optional ​

NOTE: Write-only arguments are supported in Terraform 1.11 and later.

  • password (String, Sensitive) Your own password or access token at the upstream registry. Required for upstreams whose catalog entry says requires_credentials, together with username. Never echoed back by the API and there is no update route, so changing it REPLACES the cache and deletes what it had cached. NOT VALIDATED at creation — a wrong password surfaces as a failed pull, never as an error from terraform apply. Stored in Terraform state in plaintext — sensitive redacts CLI output, not the state file. See the Secrets in Terraform state guide. Prefer password_wo, which carries the same value but is never written to state; the two are mutually exclusive.
  • password_wo (String, Sensitive, Write-only) Your own password or access token at the upstream registry, as a write-only argument: the value reaches the provider on apply and is never written to the plan or to state. Requires Terraform 1.11 or later. Mutually exclusive with password, and password_wo_version is required whenever this one is set. Terraform cannot see a write-only value, so it cannot detect a change to it: changing password_wo_version is what makes the next apply send the current value, and it does so by REPLACING the cache — deleting what it had cached — because the cache API has no update route. Editing the password without touching the version does nothing at all.
  • password_wo_version (String) Change tracker for password_wo, required whenever that attribute is set. Changing this value REPLACES the cache — deleting every image it had cached — and sends the current password_wo, which is the same lifecycle a change to password has, since the cache API has no update route. Leaving it alone leaves the stored credential untouched however much the write-only value changes. Its content is arbitrary — a counter or a date is typical — and unlike the password it IS stored in state, so do not derive it from the password or from anything in it: a digest of a secret is printed verbatim in terraform plan output, CI job logs and PR plan comments, and a digest is an offline confirmation oracle. terraform import leaves this unset, so the first apply against an imported cache that uses password_wo plans a REPLACEMENT.
  • username (String) Your own username at the upstream registry. Required for upstreams whose catalog entry says requires_credentials (Docker Hub today — read the flag, do not special-case the key); the server enforces it either way. Never echoed back by the API: no endpoint returns it, so Terraform reports whatever it last wrote and cannot detect a change made elsewhere. Changing it replaces the cache.

Read-Only ​

  • display (String) The upstream in human-readable form, naming the real host. The key is opaque because it has to be safe in a namespace, so this is the only place a person sees where the images come from. FALLS BACK TO THE BARE KEY once an upstream is retired from the catalog — the cache keeps working and stays deletable, so a display equal to the key is a retired upstream, not an error.
  • id (String) The upstream key. A tenant holds at most one cache per upstream and the API addresses a cache by that key, so the key is the identifier.
  • namespace (String) The namespace this cache occupies, <tenant-namespace>-cache-<upstream>. Derived from the immutable tenant id and the upstream key, so it never changes under a cache that is not being replaced.
  • pull_path (String) What to prefix an image with to pull it through this cache — the whole point of the resource: docker pull <pull_path>/<the upstream's own path for that image>. USE THE UPSTREAM'S PATH EXACTLY, do not shorten it. Docker Hub keeps its OFFICIAL images under library/, so docker pull <pull_path>/alpine asks Hub for a repository that does not exist and comes back a bare 401 naming nothing — docker pull <pull_path>/library/alpine:3 is the working form (measured 2026-09-05, nine failed pulls before the cause was found). The portal and the fm CLI render a working example per cache from the platform's own catalog; this provider does not read that catalog, which is why the rule is stated here instead.
  • tenant_id (String) The owning tenant (server-set; the provider's tenant_id selects it).
  • used_bytes (Number) Storage stored in THIS cache namespace, in bytes, against this namespace's own cap — the cap whose crossing makes a cached pull fail. frostmoln_container_registry.storage_used_bytes is the tenant aggregate and the figure you are metered on; this is the per-namespace detail that says which cache is close to refusing. Do not sum these and expect the aggregate: both skip a namespace whose quota could not be read. OBSERVATIONAL: a cache fills on PULL, so this moves without anyone pushing and will differ on each refresh. Null for two causes with the same result — a cache that has mirrored nothing yet, and a quota read that failed; on a failure it is null for EVERY cache rather than some, so a partial picture never reads as a complete one. Deduplicated bytes as the registry measures them, which does not equal the sum of your local image sizes.

Import ​

Import is supported using the following syntax:

The terraform import command can be used, for example:

shell
# A cache is imported by its UPSTREAM KEY. There is no tenant segment: the
# operating tenant is a provider-level selector (the provider's `tenant_id`,
# FROSTMOLN_TENANT_ID, or your default tenant), so a tenant in the id would be
# ignored.
terraform import frostmoln_container_registry_cache.ghcr ghcr

# WARNING: the upstream credentials CANNOT be imported — no endpoint returns
# them. `username`, `password`, `password_wo` and `password_wo_version` all
# arrive unset, so a configuration that sets any of them will plan a REPLACE on
# the first apply after the import, which DELETES the cache and every image
# cached in it. For an upstream that needs no credentials the import is clean;
# for one that does (Docker Hub), expect that replace and schedule it.