provisioning-catalog/components/zot/cluster/zot-lib.sh

862 lines
28 KiB
Bash
Raw Permalink Normal View History

# Methods library for zot — sourced by install-zot.sh (and the orchestrator's
# generated run-*.sh). SCRIPT_DIR and ZOT_* env must be set before sourcing.
_resolve_kubeconfig() {
if [ -n "${KUBECONFIG:-}" ] && [ -f "$KUBECONFIG" ]; then
return
fi
for candidate in \
/var/lib/k0s/pki/admin.conf \
/etc/kubernetes/admin.conf \
/root/.kube/config; do
if [ -f "$candidate" ]; then
export KUBECONFIG="$candidate"
return
fi
done
if command -v k0s >/dev/null 2>&1; then
local tmp
tmp="$(mktemp)"
k0s kubeconfig admin > "$tmp" 2>/dev/null && export KUBECONFIG="$tmp" && return
rm -f "$tmp"
fi
echo "ERROR: kubeconfig not found — set KUBECONFIG explicitly" >&2; exit 1
}
_kubectl() {
if command -v kubectl >/dev/null 2>&1; then
command kubectl "$@"
elif command -v k0s >/dev/null 2>&1; then
k0s kubectl "$@"
else
echo "ERROR: no kubectl or k0s binary found" >&2; exit 127
fi
}
_resolve_kubeconfig
_require_env() {
local var="$1"
if [ -z "${!var:-}" ]; then
echo "ERROR: required variable $var is not set" >&2
exit 1
fi
}
_namespace_exists() {
_kubectl get namespace "$ZOT_NAMESPACE" >/dev/null 2>&1
}
_wait_for_statefulset() {
local name="$1"
local timeout="${2:-300}"
_kubectl rollout status statefulset/"$name" \
--namespace "$ZOT_NAMESPACE" \
--timeout="${timeout}s"
}
_apply_cert_manager_resources() {
local cert_template="$SCRIPT_DIR/manifests/certificate.yaml"
[ -s "$cert_template" ] || return 0
_require_env ZOT_CF_DNS_TOKEN
echo " [cert-manager] applying CF DNS-01 token secret '${ZOT_CF_DNS_SECRET_NAME}' in namespace 'cert-manager'..."
_kubectl create secret generic "${ZOT_CF_DNS_SECRET_NAME}" \
--namespace cert-manager \
--from-literal=api-token="${ZOT_CF_DNS_TOKEN}" \
--dry-run=client -o yaml | _kubectl apply -f -
echo " [cert-manager] applying ClusterIssuer..."
local issuer_name
issuer_name=$(grep -m1 'name:' "$SCRIPT_DIR/manifests/clusterissuer.yaml" | awk '{print $2}')
if ! _kubectl get clusterissuer "$issuer_name" >/dev/null 2>&1; then
_kubectl apply --server-side -f "$SCRIPT_DIR/manifests/clusterissuer.yaml"
fi
echo " [cert-manager] applying Certificate (namespace=${ZOT_TLS_NAMESPACE})..."
_kubectl apply --server-side -f "$cert_template"
if [ -s "$SCRIPT_DIR/manifests/referencegrant.yaml" ]; then
echo " [cert-manager] applying ReferenceGrant (cross-namespace cert read)..."
_kubectl apply --server-side -f "$SCRIPT_DIR/manifests/referencegrant.yaml"
fi
echo " [cert-manager] waiting for TLS secret '${ZOT_TLS_SECRET}' in namespace '${ZOT_TLS_NAMESPACE}' (up to 300s)..."
local i=0
until _kubectl get secret "${ZOT_TLS_SECRET}" --namespace "${ZOT_TLS_NAMESPACE}" >/dev/null 2>&1; do
i=$((i + 1))
if [ $i -ge 60 ]; then
echo "ERROR: tls secret '${ZOT_TLS_SECRET}' not created after 300s in '${ZOT_TLS_NAMESPACE}' — check ClusterIssuer and DNS challenge" >&2
_kubectl describe certificate "${ZOT_TLS_SECRET}" --namespace "${ZOT_TLS_NAMESPACE}" 2>&1 | tail -30 >&2 || true
exit 1
fi
sleep 5
done
echo " [cert-manager] TLS secret '${ZOT_TLS_SECRET}' ready in '${ZOT_TLS_NAMESPACE}'"
}
# Redesigned 2026-07-03 (candidate remediation, R4 — see
# provisioning/.coder/2026-07-03_laws-catalogo-provisioning.plan.md §8 T4-followup).
# Was: kubectl get gateway -o json | jq (decide if listener exists) | kubectl patch.
# Now: declarative Server-Side Apply of a manifest rendered by the orchestrator
# (manifests/public-gateway-listener.yaml.j2 -> .yaml, same convention as
# clusterissuer/certificate/referencegrant above). This works safely on a Gateway
# object SHARED with other components/field-managers because Gateway API's
# spec.listeners is marked `x-kubernetes-list-type: map` (merge key: name) —
# Server-Side Apply merges by listener name instead of replacing the whole array,
# so this never touches listeners owned by other managers. No read, no decision;
# naturally idempotent (re-applying identical content is a no-op).
# --force-conflicts: the sharing-key Gateway-creation branch above may have
# already declared this same listener via plain (client-side) `kubectl apply`:
# take ownership under field-manager=zot-registry rather than erroring on the
# pre-existing client-side-apply field ownership.
_patch_public_gateway_listener() {
local gw="${ZOT_PUBLIC_GATEWAY_NAME}"
[ -z "$gw" ] && return 0
[ -z "${ZOT_GATEWAY_HOSTNAME_REG}" ] && { echo "ERROR: ZOT_GATEWAY_HOSTNAME_REG required when ZOT_PUBLIC_GATEWAY_NAME is set" >&2; exit 1; }
local manifest="$SCRIPT_DIR/manifests/public-gateway-listener.yaml"
[ -s "$manifest" ] || { echo "ERROR: $manifest not found or empty — was it rendered by the orchestrator?" >&2; exit 1; }
_kubectl apply --server-side --force-conflicts --field-manager=zot-registry -f "$manifest"
echo " [ok] gateway ${gw}: HTTPS listener https-reg applied (server-side, field-manager=zot-registry)"
}
# Redesigned 2026-07-03, same pass as the add path: declarative Server-Side Apply
# retraction. Applying an explicit empty `listeners: []` under the SAME field
# manager that added the listener (zot-registry) tells the API server that
# manager now owns zero entries in the merge-keyed (x-kubernetes-list-type=map)
# list — it prunes exactly what zot-registry previously contributed (https-reg),
# leaving listeners owned by any other field manager on this shared Gateway
# untouched. field-manager=zot-registry never declared gatewayClassName or other
# top-level Gateway fields (those are owned by whichever manager created the
# Gateway itself), so this partial apply cannot affect them. Accepted on the same
# documented Gateway API + SSA per-manager ownership basis as the add path — not
# separately exercised against a live cluster.
_remove_public_gateway_listener() {
local gw="${ZOT_PUBLIC_GATEWAY_NAME}"
local gw_ns="${ZOT_PUBLIC_GATEWAY_NS}"
[ -z "$gw" ] && return 0
_kubectl apply --server-side --force-conflicts --field-manager=zot-registry -f - <<GATEWAY
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: ${gw}
namespace: ${gw_ns}
spec:
listeners: []
GATEWAY
echo " [ok] gateway ${gw}: HTTPS listener https-reg retracted (server-side, field-manager=zot-registry)"
}
_create_public_httproute() {
local gw="${ZOT_PUBLIC_GATEWAY_NAME}"
local gw_ns="${ZOT_PUBLIC_GATEWAY_NS}"
local hostname="${ZOT_GATEWAY_HOSTNAME_REG}"
[ -z "$gw" ] && return 0
_kubectl apply -f - <<ROUTE
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: zot-public
namespace: ${ZOT_NAMESPACE}
labels:
app.kubernetes.io/name: zot
app.kubernetes.io/managed-by: provisioning
spec:
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: ${gw}
namespace: ${gw_ns}
sectionName: https-reg
hostnames:
- ${hostname}
rules:
- backendRefs:
- name: zot
port: ${ZOT_PORT}
ROUTE
echo " [ok] HTTPRoute zot-public → ${gw}/${gw_ns} for ${hostname}"
}
_generate_config_json() {
local admin_user="${ZOT_ADMIN_USER:-regadm}"
local data_dir="${ZOT_DATA_DIR:-/var/lib/data/zot}"
local port="${ZOT_PORT:-5000}"
local write_policy
write_policy=$(jq -cn --arg u "$admin_user" \
'[{"users":[$u],"actions":["read","create","update","delete","detectManifestCollision"]}]')
local repos_block
if [ -n "${ZOT_REPOSITORIES_FILE:-}" ] && [ -f "${ZOT_REPOSITORIES_FILE}" ]; then
local repos_src
repos_src=$(cat "${ZOT_REPOSITORIES_FILE}")
repos_block=$(jq -cn \
--argjson src "$repos_src" \
--argjson wp "$write_policy" \
'{"**": {anonymousPolicy: [], defaultPolicy: []}} +
($src | to_entries | map({
key: .key,
value: {
anonymousPolicy: (if .value.anonymous_read then ["read"] else [] end),
defaultPolicy: (if .value.anonymous_read then ["read"] else [] end),
policies: $wp
}
}) | from_entries)')
else
echo "WARNING: ZOT_REPOSITORIES_FILE not set — using catch-all deny only (no namespace ACL)" >&2
repos_block='{"**":{"anonymousPolicy":[],"defaultPolicy":[]}}'
fi
local cfg
cfg=$(jq -cn \
--arg data_dir "$data_dir" \
--arg port "$port" \
--argjson wp "$write_policy" \
--arg admin "$admin_user" \
--argjson repos "$repos_block" \
'{
distSpecVersion: "1.1.0",
storage: { rootDirectory: $data_dir, dedupe: false, gc: true },
http: {
address: "0.0.0.0", port: $port, realm: "zot",
readTimeout: "0s", writeTimeout: "0s",
accessControl: {
adminPolicy: { users: [$admin] },
repositories: $repos
}
},
log: { level: "info" },
extensions: { search: { enable: true }, ui: { enable: true }, trust: { enable: true, cosign: true } }
}')
if [ -n "${ZOT_S3_BUCKET:-}" ]; then
cfg=$(echo "$cfg" | jq \
--arg bucket "$ZOT_S3_BUCKET" \
--arg region "${ZOT_S3_REGION:-eu-central-1}" \
--arg endpoint "${ZOT_S3_ENDPOINT:-}" \
--argjson pstyle "${ZOT_S3_PATH_STYLE:-true}" \
'.storage.storageDriver = {name:"s3",rootdirectory:"/zot",region:$region,bucket:$bucket,secure:true,skipverify:false,forcepathstyle:$pstyle}
| if $endpoint != "" then .storage.storageDriver.regionendpoint = $endpoint else . end')
fi
if [ -n "${ZOT_HTPASSWD:-}" ]; then
cfg=$(echo "$cfg" | jq '.http.auth = {htpasswd:{path:"/etc/zot/htpasswd"},failDelay:0}')
fi
if [ "${ZOT_POD_TLS:-}" = "true" ] && [ -n "${ZOT_TLS_SECRET:-}" ]; then
cfg=$(echo "$cfg" | jq '.http.tls = {cert:"/etc/zot/ssl/tls.crt",key:"/etc/zot/ssl/tls.key"}')
fi
if [ -n "${ZOT_SYNC_UPSTREAM:-}" ]; then
local prefixes_json
prefixes_json=$(printf '%s' "${ZOT_SYNC_UPSTREAM_PREFIXES:-domains/**,modes/**}" \
| tr ',' '\n' | sed 's/^ *//;s/ *$//' | jq -Rn '[inputs] | map({prefix:.})')
cfg=$(echo "$cfg" | jq \
--arg u "$ZOT_SYNC_UPSTREAM" \
--argjson tls "${ZOT_SYNC_UPSTREAM_TLS:-true}" \
--argjson od "${ZOT_SYNC_ON_DEMAND:-true}" \
--argjson content "$prefixes_json" \
--arg dl "${ZOT_DATA_DIR:-/var/lib/data/zot}/sync-tmp" \
'.extensions.sync = {downloadDir:$dl,credentialsFile:"/etc/zot/sync-creds.json",
registries:[{urls:[$u],tlsVerify:$tls,onDemand:$od,content:$content}]}')
fi
echo "$cfg"
}
_apply_inline_manifests() {
local config_json
config_json=$(_generate_config_json)
_kubectl apply -f - <<MANIFEST
apiVersion: v1
kind: Namespace
metadata:
name: ${ZOT_NAMESPACE}
labels:
app.kubernetes.io/managed-by: provisioning
MANIFEST
_kubectl apply -f - <<MANIFEST
apiVersion: v1
kind: ConfigMap
metadata:
name: zot-config
namespace: ${ZOT_NAMESPACE}
labels:
app.kubernetes.io/name: zot
app.kubernetes.io/managed-by: provisioning
data:
config.json: |
$(echo "$config_json" | sed 's/^/ /')
MANIFEST
if [ -z "${ZOT_S3_BUCKET:-}" ]; then
if ! _kubectl get pvc zot-data --namespace "$ZOT_NAMESPACE" >/dev/null 2>&1; then
_kubectl apply -f - <<MANIFEST
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: zot-data
namespace: ${ZOT_NAMESPACE}
labels:
app.kubernetes.io/name: zot
app.kubernetes.io/managed-by: provisioning
annotations:
provisioning.io/lifecycle: durable
spec:
accessModes:
- ReadWriteOnce
storageClassName: ${ZOT_STORAGE_CLASS}
resources:
requests:
storage: ${ZOT_STORAGE_SIZE}
MANIFEST
else
echo " [skip] PVC zot-data already exists — lifecycle=durable, not resized"
fi
else
echo " [skip] PVC — storage backend is S3 (${ZOT_S3_BUCKET}), using emptyDir for local cache"
fi
# StatefulSet with conditional volume mounts/volumes
local auth_mount="" auth_vol="" tls_mount="" tls_vol="" sync_mount="" sync_vol="" data_vol=""
if [ -n "${ZOT_HTPASSWD:-}" ]; then
auth_mount=$(printf ' - name: auth\n mountPath: /etc/zot/htpasswd\n subPath: htpasswd\n readOnly: true')
auth_vol=$(printf ' - name: auth\n secret:\n secretName: zot-auth\n items:\n - key: htpasswd\n path: htpasswd')
fi
if [ "${ZOT_POD_TLS:-}" = "true" ] && [ -n "${ZOT_TLS_SECRET:-}" ]; then
tls_mount=$(printf ' - name: tls\n mountPath: /etc/zot/ssl\n readOnly: true')
tls_vol=$(printf ' - name: tls\n secret:\n secretName: %s\n items:\n - key: tls.crt\n path: tls.crt\n - key: tls.key\n path: tls.key' "$ZOT_TLS_SECRET")
fi
if [ -n "${ZOT_SYNC_UPSTREAM:-}" ]; then
sync_mount=$(printf ' - name: sync-creds\n mountPath: /etc/zot/sync-creds.json\n subPath: sync-creds.json\n readOnly: true')
sync_vol=$(printf ' - name: sync-creds\n secret:\n secretName: zot-sync-creds\n items:\n - key: sync-creds.json\n path: sync-creds.json')
fi
if [ -n "${ZOT_S3_BUCKET:-}" ]; then
data_vol=' - name: data
emptyDir: {}'
else
data_vol=' - name: data
persistentVolumeClaim:
claimName: zot-data'
fi
_kubectl apply -f - <<MANIFEST
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zot
namespace: ${ZOT_NAMESPACE}
labels:
app.kubernetes.io/name: zot
app.kubernetes.io/managed-by: provisioning
spec:
serviceName: zot
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: zot
template:
metadata:
labels:
app.kubernetes.io/name: zot
spec:
containers:
- name: zot
image: ${ZOT_IMAGE}
args: ["serve", "/etc/zot/config.json"]
ports:
- containerPort: ${ZOT_PORT}
name: registry
env:
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: zot-s3-credentials
key: access_key
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: zot-s3-credentials
key: secret_key
- name: AWS_REGION
value: "${ZOT_S3_REGION:-eu-central-1}"
volumeMounts:
- name: config
mountPath: /etc/zot/config.json
subPath: config.json
readOnly: true
- name: data
mountPath: ${ZOT_DATA_DIR}
${auth_mount:+${auth_mount}}
${tls_mount:+${tls_mount}}
${sync_mount:+${sync_mount}}
readinessProbe:
tcpSocket:
port: ${ZOT_PORT}
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
tcpSocket:
port: ${ZOT_PORT}
initialDelaySeconds: 30
periodSeconds: 20
volumes:
- name: config
configMap:
name: zot-config
${data_vol}
${auth_vol:+${auth_vol}}
${tls_vol:+${tls_vol}}
${sync_vol:+${sync_vol}}
MANIFEST
_kubectl apply -f - <<MANIFEST
apiVersion: v1
kind: Service
metadata:
name: zot
namespace: ${ZOT_NAMESPACE}
labels:
app.kubernetes.io/name: zot
app.kubernetes.io/managed-by: provisioning
spec:
selector:
app.kubernetes.io/name: zot
type: ClusterIP
ports:
- name: registry
port: ${ZOT_PORT}
targetPort: ${ZOT_PORT}
MANIFEST
}
_do_install() {
_require_env ZOT_S3_ACCESS_KEY
_require_env ZOT_S3_SECRET_KEY
_apply_cert_manager_resources
_kubectl create secret generic zot-s3-credentials \
--namespace "$ZOT_NAMESPACE" \
--from-literal=access_key="${ZOT_S3_ACCESS_KEY}" \
--from-literal=secret_key="${ZOT_S3_SECRET_KEY}" \
--dry-run=client -o yaml | _kubectl apply -f -
if [ -n "${ZOT_HTPASSWD}" ]; then
_kubectl create secret generic zot-auth \
--namespace "$ZOT_NAMESPACE" \
--from-literal=htpasswd="${ZOT_HTPASSWD}" \
--dry-run=client -o yaml | _kubectl apply -f -
fi
if [ -n "${ZOT_SYNC_UPSTREAM}" ]; then
local sync_creds="{}"
if [ -n "${ZOT_SYNC_USERNAME}" ] && [ -n "${ZOT_SYNC_PASSWORD}" ]; then
# zot credential file keys are hostname-only (no scheme). Strip https:// or http://.
local sync_host="${ZOT_SYNC_UPSTREAM#https://}"
sync_host="${sync_host#http://}"
sync_creds=$(jq -cn \
--arg host "$sync_host" \
--arg user "${ZOT_SYNC_USERNAME}" \
--arg pass "${ZOT_SYNC_PASSWORD}" \
'{($host): {username: $user, password: $pass}}')
fi
_kubectl create secret generic zot-sync-creds \
--namespace "$ZOT_NAMESPACE" \
--from-literal=sync-creds.json="${sync_creds}" \
--dry-run=client -o yaml | _kubectl apply -f -
fi
_apply_inline_manifests
_kubectl rollout restart statefulset/zot --namespace "$ZOT_NAMESPACE"
echo "Waiting for zot StatefulSet to be ready..."
_wait_for_statefulset "zot" 300
echo "zot installed in namespace ${ZOT_NAMESPACE}"
echo " ClusterIP port : ${ZOT_PORT}"
echo " S3 bucket : ${ZOT_S3_BUCKET} @ ${ZOT_S3_ENDPOINT:-AWS}"
[ -n "${ZOT_HTPASSWD}" ] && echo " Auth : htpasswd enabled"
if [ "${ZOT_GATEWAY_ENABLED}" = "true" ]; then
[ -z "${ZOT_GATEWAY_IP}" ] && { echo "ERROR: ZOT_GATEWAY_ENABLED=true requires ZOT_GATEWAY_IP" >&2; exit 1; }
# Dedicated IP pool: skipped when sharing an existing LB IP (ZOT_GATEWAY_SHARING_KEY set).
# Sharing mode uses infrastructure.annotations (io.cilium/lb-ipam-ips +
# lbipam.cilium.io/sharing-key) so the existing pool for that IP is reused.
if [ -z "${ZOT_GATEWAY_SHARING_KEY}" ]; then
_kubectl apply -f - <<IPPOOL
---
apiVersion: cilium.io/v2
kind: CiliumLoadBalancerIPPool
metadata:
name: registry-gateway-pool
labels:
app.kubernetes.io/name: registry-gateway
app.kubernetes.io/managed-by: provisioning
spec:
blocks:
- cidr: "${ZOT_GATEWAY_IP}/32"
serviceSelector:
matchLabels:
io.cilium.gateway/owning-gateway: registry
IPPOOL
fi
# Cross-namespace cert reference: when the TLS secret lives outside the gateway namespace,
# the listener must declare the namespace explicitly (Gateway API §SecretObjectReference).
# ReferenceGrant in the secret namespace authorizes this read (emitted by referencegrant.yaml).
local cert_ref_ns_line=""
if [ "${ZOT_TLS_NAMESPACE}" != "${ZOT_GATEWAY_NAMESPACE}" ]; then
cert_ref_ns_line=$'\n namespace: '"${ZOT_TLS_NAMESPACE}"
fi
case "${ZOT_GATEWAY_MODE}" in
terminate)
[ -z "${ZOT_GATEWAY_CERT_SECRET}" ] && { echo "ERROR: mode=terminate requires ZOT_GATEWAY_CERT_SECRET" >&2; exit 1; }
if [ -n "${ZOT_GATEWAY_SHARING_KEY}" ]; then
_kubectl apply -f - <<GATEWAY
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: registry
namespace: ${ZOT_GATEWAY_NAMESPACE}
labels:
app.kubernetes.io/name: registry-gateway
app.kubernetes.io/managed-by: provisioning
spec:
gatewayClassName: cilium
infrastructure:
annotations:
io.cilium/lb-ipam-ips: "${ZOT_GATEWAY_IP}"
lbipam.cilium.io/sharing-key: "${ZOT_GATEWAY_SHARING_KEY}"
listeners:
- name: https-ui
hostname: "${ZOT_GATEWAY_HOSTNAME_UI}"
port: 443
protocol: HTTPS
tls:
mode: Terminate
certificateRefs:
- name: ${ZOT_GATEWAY_CERT_SECRET}${cert_ref_ns_line}
allowedRoutes:
namespaces:
from: All
- name: https-reg
hostname: "${ZOT_GATEWAY_HOSTNAME_REG}"
port: 443
protocol: HTTPS
tls:
mode: Terminate
certificateRefs:
- name: ${ZOT_GATEWAY_CERT_SECRET}${cert_ref_ns_line}
allowedRoutes:
namespaces:
from: All
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: zot
namespace: ${ZOT_NAMESPACE}
labels:
app.kubernetes.io/name: zot
app.kubernetes.io/managed-by: provisioning
spec:
parentRefs:
- name: registry
namespace: ${ZOT_GATEWAY_NAMESPACE}
hostnames:
- ${ZOT_GATEWAY_HOSTNAME_UI}
- ${ZOT_GATEWAY_HOSTNAME_REG}
rules:
- backendRefs:
- name: zot
port: ${ZOT_PORT}
GATEWAY
else
_kubectl apply -f - <<GATEWAY
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: registry
namespace: ${ZOT_GATEWAY_NAMESPACE}
labels:
app.kubernetes.io/name: registry-gateway
app.kubernetes.io/managed-by: provisioning
spec:
gatewayClassName: cilium
addresses:
- type: IPAddress
value: "${ZOT_GATEWAY_IP}"
listeners:
- name: https
port: 443
protocol: HTTPS
tls:
mode: Terminate
certificateRefs:
- name: ${ZOT_GATEWAY_CERT_SECRET}${cert_ref_ns_line}
allowedRoutes:
namespaces:
from: All
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: zot
namespace: ${ZOT_NAMESPACE}
labels:
app.kubernetes.io/name: zot
app.kubernetes.io/managed-by: provisioning
spec:
parentRefs:
- name: registry
namespace: ${ZOT_GATEWAY_NAMESPACE}
hostnames:
- ${ZOT_GATEWAY_HOSTNAME_UI}
- ${ZOT_GATEWAY_HOSTNAME_REG}
rules:
- backendRefs:
- name: zot
port: ${ZOT_PORT}
GATEWAY
fi
echo " Gateway : registry → ${ZOT_GATEWAY_IP} (TLS terminate :443 → HTTP :${ZOT_PORT})"
;;
passthrough)
if [ -n "${ZOT_GATEWAY_SHARING_KEY}" ]; then
_kubectl apply -f - <<GATEWAY
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: registry
namespace: ${ZOT_GATEWAY_NAMESPACE}
labels:
app.kubernetes.io/name: registry-gateway
app.kubernetes.io/managed-by: provisioning
spec:
gatewayClassName: cilium
infrastructure:
annotations:
io.cilium/lb-ipam-ips: "${ZOT_GATEWAY_IP}"
lbipam.cilium.io/sharing-key: "${ZOT_GATEWAY_SHARING_KEY}"
listeners:
- name: https-passthrough-ui
hostname: "${ZOT_GATEWAY_HOSTNAME_UI}"
port: 443
protocol: TLS
tls:
mode: Passthrough
allowedRoutes:
namespaces:
from: All
kinds:
- group: gateway.networking.k8s.io
kind: TLSRoute
- name: https-passthrough-reg
hostname: "${ZOT_GATEWAY_HOSTNAME_REG}"
port: 443
protocol: TLS
tls:
mode: Passthrough
allowedRoutes:
namespaces:
from: All
kinds:
- group: gateway.networking.k8s.io
kind: TLSRoute
---
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
name: zot
namespace: ${ZOT_NAMESPACE}
labels:
app.kubernetes.io/name: zot
app.kubernetes.io/managed-by: provisioning
spec:
parentRefs:
- name: registry
namespace: ${ZOT_GATEWAY_NAMESPACE}
hostnames:
- ${ZOT_GATEWAY_HOSTNAME_UI}
- ${ZOT_GATEWAY_HOSTNAME_REG}
rules:
- backendRefs:
- name: zot
port: ${ZOT_PORT}
GATEWAY
else
_kubectl apply -f - <<GATEWAY
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: registry
namespace: ${ZOT_GATEWAY_NAMESPACE}
labels:
app.kubernetes.io/name: registry-gateway
app.kubernetes.io/managed-by: provisioning
spec:
gatewayClassName: cilium
addresses:
- type: IPAddress
value: "${ZOT_GATEWAY_IP}"
listeners:
- name: https-passthrough
port: 443
protocol: TLS
tls:
mode: Passthrough
allowedRoutes:
namespaces:
from: All
kinds:
- group: gateway.networking.k8s.io
kind: TLSRoute
---
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
name: zot
namespace: ${ZOT_NAMESPACE}
labels:
app.kubernetes.io/name: zot
app.kubernetes.io/managed-by: provisioning
spec:
parentRefs:
- name: registry
namespace: ${ZOT_GATEWAY_NAMESPACE}
hostnames:
- ${ZOT_GATEWAY_HOSTNAME_UI}
- ${ZOT_GATEWAY_HOSTNAME_REG}
rules:
- backendRefs:
- name: zot
port: ${ZOT_PORT}
GATEWAY
fi
echo " Gateway : registry → ${ZOT_GATEWAY_IP} (TLS passthrough :443)"
;;
*)
echo "ERROR: unknown ZOT_GATEWAY_MODE '${ZOT_GATEWAY_MODE}'. Valid: terminate | passthrough" >&2
exit 1
;;
esac
echo " UI : https://${ZOT_GATEWAY_HOSTNAME_UI}"
echo " Registry : https://${ZOT_GATEWAY_HOSTNAME_REG}"
_patch_public_gateway_listener
_create_public_httproute
fi
_upload_cosign_trust_key
}
# Upload cosign public key to zot's imagetrust trust store.
# API: POST /v2/_zot/ext/cosign (Content-Type: text/plain, PEM public key body).
# Skipped when ZOT_COSIGN_PUBKEY_PATH is unset or ZOT_ADMIN_PASS is empty.
# Uses ZOT_GATEWAY_HOSTNAME_REG via curl — never propagates failures.
_upload_cosign_trust_key() {
[ -z "${ZOT_COSIGN_PUBKEY_PATH:-}" ] && return 0
if [ ! -f "${ZOT_COSIGN_PUBKEY_PATH}" ]; then
echo "WARNING: ZOT_COSIGN_PUBKEY_PATH=${ZOT_COSIGN_PUBKEY_PATH} not found — skipping imagetrust upload" >&2
return 0
fi
if [ -z "${ZOT_ADMIN_PASS:-}" ]; then
echo "WARNING: ZOT_ADMIN_PASS not set — skipping imagetrust cosign key upload" >&2
return 0
fi
local upload_url="https://${ZOT_GATEWAY_HOSTNAME_REG}/v2/_zot/ext/cosign"
echo " [imagetrust] uploading cosign public key → ${upload_url}..."
local http_code curl_exit
set +e
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "${upload_url}" \
-u "${ZOT_ADMIN_USER}:${ZOT_ADMIN_PASS}" \
-H "Content-Type: text/plain" \
--data-binary "@${ZOT_COSIGN_PUBKEY_PATH}" 2>/dev/null)
curl_exit=$?
set -e
if [ "${curl_exit}" -ne 0 ]; then
echo "WARNING: curl failed (exit ${curl_exit}) reaching ${ZOT_GATEWAY_HOSTNAME_REG} — imagetrust key upload skipped" >&2
return 0
fi
if [ "${http_code}" = "200" ] || [ "${http_code}" = "201" ] || [ "${http_code}" = "204" ]; then
echo " [imagetrust] cosign public key uploaded (HTTP ${http_code})"
else
echo "WARNING: imagetrust cosign key upload returned HTTP ${http_code} — run manually: POST ${upload_url}" >&2
fi
}
_do_update() {
_kubectl set image statefulset/zot \
zot="${ZOT_IMAGE}" \
--namespace "$ZOT_NAMESPACE"
echo "Waiting for zot rollout..."
_wait_for_statefulset "zot" 300
echo "zot updated to image ${ZOT_IMAGE}"
}
_do_delete() {
if _namespace_exists; then
echo "WARNING: this deletes the zot StatefulSet but NOT the PVC (durable lifecycle)"
_kubectl delete statefulset zot --namespace "$ZOT_NAMESPACE" --timeout=60s || true
_kubectl delete service zot --namespace "$ZOT_NAMESPACE" --timeout=30s || true
_kubectl delete configmap zot-config --namespace "$ZOT_NAMESPACE" --timeout=30s || true
_kubectl delete httproute zot --namespace "$ZOT_NAMESPACE" --timeout=30s 2>/dev/null || true
_kubectl delete httproute zot-public --namespace "$ZOT_NAMESPACE" --timeout=30s 2>/dev/null || true
_kubectl delete tlsroute zot --namespace "$ZOT_NAMESPACE" --timeout=30s 2>/dev/null || true
echo "zot removed — PVC and S3 data preserved"
else
echo "Namespace ${ZOT_NAMESPACE} does not exist, nothing to delete"
fi
_kubectl delete gateway registry --namespace "${ZOT_GATEWAY_NAMESPACE}" --timeout=30s || true
_remove_public_gateway_listener
}
_do_health() {
local pod
pod=$(_kubectl get pod --namespace "$ZOT_NAMESPACE" \
--selector app.kubernetes.io/name=zot \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [ -z "$pod" ]; then
echo "UNHEALTHY: no zot pod found in namespace ${ZOT_NAMESPACE}" >&2
exit 1
fi
local scheme="http"
local wget_tls_flag=""
if [ -n "${ZOT_TLS_SECRET}" ]; then
scheme="https"
wget_tls_flag="--no-check-certificate"
fi
local status
status=$(_kubectl exec --namespace "$ZOT_NAMESPACE" "$pod" -- \
wget -qO- --server-response ${wget_tls_flag} "${scheme}://localhost:${ZOT_PORT}/v2/" 2>&1 \
| grep "HTTP/" | tail -1 | awk '{print $2}' || echo "")
if [ "$status" = "200" ] || [ "$status" = "401" ]; then
echo "HEALTHY: zot /v2/ returned HTTP ${status}"
else
echo "UNHEALTHY: zot /v2/ returned HTTP ${status} (expected 200 or 401)" >&2
exit 1
fi
}