{ "$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" }, "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"] } } }