{ "$schema": "https://json-schema.org/draft-07/schema#", "title": "garage values", "description": "Strict values schema (issue #68). additionalProperties is false on every chart-owned object level, so a key this chart does not read is rejected at render time instead of being silently ignored; the objects that go verbatim into the Kubernetes pod spec (probes, security contexts, global, free-form annotation maps) stay open, because the chart does not interpret their contents. The blocks the chart dereferences additionally accept null, because an erased block means 'chart defaults apply' (issue #99) rather than a vanished setting. Note what this schema CANNOT enforce: secrets.rpcSecretRef ships as null and JSON Schema's 'required' tests presence, not null-ness, so it would never fire - the enforcement is required() in the template (issue #18).", "type": "object", "additionalProperties": false, "properties": { "global": { "description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.", "type": "object" }, "networkPolicy": { "description": "The chart's NetworkPolicy (Gitea #5), on by default. A ceiling on reachability and not an authentication layer: only the declared container ports are reachable at all, egress is capped by PORT (a NetworkPolicy matches CIDRs and selectors, never hostnames, so a destination host cannot be expressed), and ingress is capped by source for every port no network probe touches. Only a boundary where the cluster's CNI enforces policies - an unenforced policy is accepted by the API server and filters nothing. The template's header carries the full list of what it does not do.", "type": "object", "additionalProperties": false, "properties": { "enabled": { "type": "boolean" }, "allowedNamespaces": { "description": "Namespaces whose pods may reach this workload, in addition to the release's own namespace, which is always allowed. Matched on the kubernetes.io/metadata.name label Kubernetes sets on every namespace itself, so no labelling work is needed - and no typo protection either: a name that does not exist matches nothing and is silently ineffective. An entry opens the port to EVERY pod in that namespace; for the ingress controller use ingressController instead.", "type": ["array", "null"], "items": { "type": "string" } }, "probeSources": { "description": "Source CIDRs allowed to reach the port the network probes address. EMPTY MEANS OPEN, not blocked: with no entry that rule is rendered without a \"from\" selector. The kubelet's probes come from the IP of the worker node running the pod, an address the chart cannot know and must not guess - a guessed CIDR that excludes the real node blocks every probe and the pod never becomes Ready, which reads as a broken application rather than as a policy. Setting this restricts the rule, which is why the chart adds the own namespace, ingressController and allowedNamespaces to the same rule.", "type": ["array", "null"], "items": { "type": "string" } }, "ingressController": { "description": "The ingress controller allowed to reach the routed port once probeSources restricts it; rendered only while ingress.enabled or httpRoute.enabled is true. The two keys become ONE \"from\" entry and are therefore an AND - pods carrying podLabels IN namespace - which is the whole point: the controller's namespace in allowedNamespaces would open the port to every pod in kube-system. The default is measured on this cluster (DaemonSet rke2-traefik in kube-system, which also serves the Gateway API here), not assumed; a policy written against the wrong controller is accepted, looks strict and silently blocks all web traffic.", "type": ["object", "null"], "additionalProperties": false, "properties": { "namespace": { "type": ["string", "null"] }, "podLabels": { "description": "Label set the controller's pods carry, rendered verbatim as a podSelector's matchLabels. Kept open because the chart does not interpret the keys.", "type": ["object", "null"] } } }, "egress": { "type": ["object", "null"], "additionalProperties": false, "properties": { "extraPorts": { "description": "Additional outbound TCP ports, to ANY destination - ports and not destinations, because a NetworkPolicy cannot express a hostname. DNS is always allowed and has no switch. Without an entry here an outbound port the chart does not already derive is blocked, which is the cap's whole purpose and also its upgrade risk.", "type": ["array", "null"], "items": { "type": "integer" } } } } } }, "scaleDown": { "description": "true renders the StatefulSet with replicas: 0 and takes precedence over garage.replicas.", "type": "boolean" }, "secrets": { "type": "object", "additionalProperties": false, "properties": { "rpcSecretRef": { "description": "op:// item path. The item must carry the field 'rpc-secret' with 32 bytes of random hex. Required always - Garage refuses to start without an RPC secret even as a single node.", "type": ["string", "null"] }, "s3SecretRef": { "description": "op:// item path. The item must carry the fields 'access-key', 'secret-key' and 'bucket'. Required while garage.singleNode is true together with defaultAccessKey or defaultBucket, because Garage imports those values on start.", "type": ["string", "null"] } } }, "garage": { "type": "object", "additionalProperties": false, "properties": { "replicas": { "description": "0 or 1. Capped at 1 because more than one replica is not a cluster: this chart renders no peer bootstrapping and no discovery CRD, so a second pod would be a lone node with its own empty metadata database.", "type": "integer", "minimum": 0, "maximum": 1 }, "singleNode": { "description": "Run 'garage server --single-node', which creates the cluster layout in-process before the listeners open. Idempotent and guarded by the layout version; requires replicationFactor 1.", "type": "boolean" }, "defaultAccessKey": { "description": "Import the S3 access key from the vault item on start (--default-access-key). Requires singleNode; the chart fails the render on the mixed combination, because Garage refuses it at start.", "type": "boolean" }, "defaultBucket": { "description": "Create the bucket from the vault item's 'bucket' field on start (--default-bucket) and grant the imported key access to it. Requires singleNode.", "type": "boolean" }, "replicationFactor": { "description": "1 for this deployment, and raising it is not an upgrade path: the factor is part of the cluster layout, so increasing it is a layout operation with data movement on a cluster that would first need more nodes.", "type": "integer", "minimum": 1 }, "dbEngine": { "description": "Metadata engine. Changing it on an existing release needs 'garage convert-db', not a values change.", "type": "string", "enum": ["lmdb", "sqlite", "fjall"] }, "metadataFsync": { "description": "true here, against Garage's own default of false: the volume layer cannot help with a write the engine never handed to the kernel.", "type": "boolean" }, "dataFsync": { "description": "false, Garage's default: a lost block is one object and is replaceable, a lost metadata write can take the index with it.", "type": "boolean" }, "metadataAutoSnapshotInterval": { "description": "Go-style duration, e.g. '6h'. The only consistent recovery point this deployment has; an empty string is rejected because switching the snapshots off is not a configuration this chart offers at replication factor 1.", "type": "string", "minLength": 1 }, "blockSize": { "type": "string", "minLength": 1 }, "compressionLevel": { "description": "zstd level for stored blocks, or the literal string \"none\" to disable compression. NOT 0 for disabling: Garage accepts 0 as an integer and zstd reads it as its default level (3), i.e. more compression than 1, not none. Negative integers are zstd's fast modes.", "oneOf": [ { "type": "integer" }, { "type": "string", "enum": ["none"] } ] }, "bindAddress": { "description": "Host part of the S3, web and RPC listeners. '[::]' accepts IPv4 as well while net.ipv6.bindv6only is 0.", "type": "string", "minLength": 1 }, "adminBindAddress": { "description": "Host part of the admin listener, separate from the others because all three probes use it.", "type": "string", "minLength": 1 }, "ports": { "description": "May be null / empty: the chart then falls back to its defaults. The ConfigMap renders these into garage.toml's bind addresses and the StatefulSet renders them as containerPorts, so both files follow one source.", "type": ["object", "null"], "additionalProperties": false, "properties": { "s3": { "$ref": "#/definitions/port" }, "rpc": { "$ref": "#/definitions/port" }, "web": { "$ref": "#/definitions/port" }, "admin": { "$ref": "#/definitions/port" } } }, "s3": { "type": ["object", "null"], "additionalProperties": false, "properties": { "region": { "description": "A label both sides must agree on; a consumer configured for a different one gets a signature error.", "type": "string", "minLength": 1 }, "rootDomain": { "description": "Optional suffix for vhost-style bucket access. Deliberately not coupled to ingress.domain or httpRoute.hostnames - a wildcard vhost domain and the single host a route answers on are different things.", "type": ["string", "null"] } } }, "web": { "type": ["object", "null"], "additionalProperties": false, "properties": { "enabled": { "description": "Static website hosting (the s3_web endpoint). Off: a second HTTP listener that serves nothing without a rootDomain.", "type": "boolean" }, "rootDomain": { "type": ["string", "null"] } } }, "resources": { "$ref": "#/definitions/resources" }, "livenessProbe": { "$ref": "#/definitions/probe" }, "readinessProbe": { "$ref": "#/definitions/probe" }, "startupProbe": { "$ref": "#/definitions/probe" }, "securityContext": { "$ref": "#/definitions/securityContext" }, "env": { "$ref": "#/definitions/env" } } }, "storage": { "description": "The two PVCs. Deliberately NOT null-safe: an erased block yields an invalid PVC, which fails loudly at apply time. Issue #99 covers only the silent class, and on an existing release a chart-side fallback could not help anyway, because a PVC cannot shrink.", "type": "object", "additionalProperties": false, "properties": { "data": { "description": "metadata_dir AND data_dir as two subdirectories of one volume, so a volume snapshot catches index and blocks atomically.", "type": "object", "additionalProperties": false, "properties": { "amount": { "type": ["string", "null"] }, "storageClass": { "type": ["string", "null"] } } }, "snapshots": { "description": "metadata_snapshots_dir, on a second volume so the recovery point does not die with the volume whose corruption it should repair.", "type": "object", "additionalProperties": false, "properties": { "amount": { "type": ["string", "null"] }, "storageClass": { "type": ["string", "null"] } } } } }, "ingress": { "type": "object", "additionalProperties": false, "properties": { "enabled": { "description": "Gates the whole Ingress template (issue #93), and ships false. Mutually exclusive with httpRoute.enabled - the render aborts when both are true. Checked in both directions for this chart, and the answer moved with Gitea #114: the HTTPRoute reads ingress.domain too (as its single hostname), so domain is free only while BOTH routing modes are off; clusterIssuer is read by the Ingress alone and ingress.enabled: false still frees it.", "type": "boolean" }, "clusterIssuer": { "type": ["string", "null"] }, "domain": { "type": ["string", "null"] }, "className": { "type": ["string", "null"] }, "annotations": { "description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open. No counterpart in Gateway mode: the Gateway API has no annotation channel and no notion of a body size, so the proxy-body-size an S3 endpoint needs cannot be set here when httpRoute.enabled is used - it has to be raised at the Gateway.", "type": "object" } } }, "httpRoute": { "description": "Gateway-API alternative to the Ingress (Gitea #114), and off by default like the Ingress here. Mutually exclusive with ingress.enabled - the render aborts when both are true. The chart renders only an HTTPRoute with parentRefs, pointing at the Service's s3 port; class and certificate sit on the Gateway and belong to the platform. The Gateway-API CRDs must be present in the cluster, because the chart cannot detect them.", "type": "object", "additionalProperties": false, "properties": { "enabled": { "type": "boolean" }, "parentRefs": { "description": "The Gateway(s) the route attaches to. Required while httpRoute.enabled is true, enforced by required()-equivalent in the template (an empty list aborts the render) - a JSON Schema \"required\" cannot do it, because the key ships as an empty list and is therefore present. Entries are the Gateway API's ParentReference and are passed through unchanged.", "type": "array", "items": { "$ref": "#/definitions/parentRef" } }, "hostnames": { "description": "Empty means [ingress.domain], so the hostname has a single source of truth. A wildcard for vhost-style bucket access goes here, alongside garage.s3.rootDomain.", "type": "array", "items": { "type": "string" } } } } }, "definitions": { "parentRef": { "description": "A Gateway API ParentReference, passed verbatim into HTTPRoute.spec.parentRefs. Deliberately left OPEN (no additionalProperties: false), for the same reason probes and security contexts are: Kubernetes owns this schema, the Gateway API adds fields to it across releases, and a closed list here would reject a field only upstream knows about. What the chart can assert is that \"name\" is there - it is required by the API and a reference without it attaches to nothing, which is the silent failure this chart aborts the render over.", "type": "object", "required": ["name"] }, "port": { "type": ["integer", "null"], "minimum": 1, "maximum": 65535 }, "resources": { "description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.", "type": ["object", "null"], "additionalProperties": false, "properties": { "limitFactor": { "type": "number", "exclusiveMinimum": 0 }, "requests": { "$ref": "#/definitions/resourceList" }, "limits": { "$ref": "#/definitions/resourceList" } } }, "resourceList": { "description": "Both spellings of the ephemeral-storage key are accepted because the resources helper normalizes camelCase to the Kubernetes key.", "type": ["object", "null"], "additionalProperties": false, "properties": { "cpu": { "type": ["string", "number", "null"] }, "memory": { "type": ["string", "number", "null"] }, "ephemeral-storage": { "type": ["string", "number", "null"] }, "ephemeralStorage": { "type": ["string", "number", "null"] } } }, "env": { "description": "Additive container env entries. An entry repeating a name the chart already sets does not replace it - on an existing release the pair collapses to the chart's own value while helm upgrade reports success.", "type": ["array", "null"], "items": { "type": "object", "additionalProperties": false, "required": ["name"], "properties": { "name": { "type": "string" }, "value": { "type": "string" }, "valueFrom": { "type": "object", "additionalProperties": false, "properties": { "secretKeyRef": { "$ref": "#/definitions/keyRef" }, "configMapKeyRef": { "$ref": "#/definitions/keyRef" } } } } } }, "keyRef": { "type": "object", "additionalProperties": false, "required": ["name", "key"], "properties": { "name": { "type": "string" }, "key": { "type": "string" } } }, "securityContext": { "description": "May be null / empty at any level: the chart then falls back to its hardening defaults (issue #99). This chart's fallback reproduces ITS defaults, deviations included - fsGroupChangePolicy: OnRootMismatch rather than the repo's Always, because an object store's data directory holds one file per block and a recursive chown on every pod start turns a restart into a multi-minute outage that grows with the store.", "type": ["object", "null"], "additionalProperties": false, "properties": { "pod": { "description": "Merged onto the chart's pod hardening defaults and rendered as the pod's securityContext; kept open because the chart does not interpret the individual fields.", "type": ["object", "null"] }, "container": { "description": "Merged onto the chart's container hardening defaults and rendered as the container's securityContext; kept open because the chart does not interpret the individual fields.", "type": ["object", "null"] } } }, "probe": { "description": "Merged onto the chart's probe defaults and rendered as a container probe; kept open because the chart does not interpret the individual fields. May be null / empty, which means the defaults apply (issue #99) rather than the probe disappearing. The defaults carry the probe's CONTEXT: the port NAME 'admin' for all three, /health for readiness only, and a TCP connect for liveness and startup - because /health answers 503 until a cluster layout exists, and a probe that can only fail would restart a node forever.", "type": ["object", "null"] } } }