{ "$schema": "https://json-schema.org/draft-07/schema#", "title": "keycloak 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.databaseSecretRef, keycloak.db.host, ingress.domain and ingress.admin.domain all ship as null and JSON Schema's 'required' tests presence, not null-ness, so a 'required' array would never fire - the enforcement is required() in the templates (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 Deployment with replicas: 0 and takes precedence over keycloak.replicas. Also the first half of the minor-upgrade procedure, because Keycloak supports a rolling update only inside a minor line.", "type": "boolean" }, "secrets": { "type": "object", "additionalProperties": false, "properties": { "databaseSecretRef": { "description": "op:// item path. The item must carry the fields 'username' and 'password' of the PostgreSQL role, which has to be able to create its own schema. Always required - the chart has no default database.", "type": ["string", "null"] }, "bootstrapAdminSecretRef": { "description": "op:// item path. The item must carry the fields 'username' and 'password' of the TEMPORARY bootstrap administrator. Required while keycloak.bootstrapAdmin.enabled is true.", "type": ["string", "null"] } } }, "keycloak": { "type": "object", "additionalProperties": false, "properties": { "replicas": { "description": "Default 2, because an SSO outage takes every dependent login with it. An explicit 0 is allowed and is the per-workload way to stop the release. Above 1 the rolling-update restriction applies: a minor version jump needs the whole cluster down.", "type": "integer", "minimum": 0 }, "db": { "description": "May be null / empty: the chart then falls back to its defaults. The host has no default and is enforced by required() in the Deployment.", "type": ["object", "null"], "additionalProperties": false, "properties": { "vendor": { "description": "A Keycloak BUILD option - it is passed to `kc.sh build` in the init container, never to the serving container. dev-file and dev-mem are rejected on purpose: they are Keycloak's development defaults, and an identity provider on an embedded database works until the pod moves. Only 'postgres' is exercised by this chart's CI.", "type": "string", "enum": ["postgres", "mariadb", "mysql", "mssql", "oracle"] }, "host": { "description": "Host of the external PostgreSQL. Required - enforced by required() in the Deployment, because a null here would render an empty KC_DB_URL_HOST and a pod that waits on a connection it can never make.", "type": ["string", "null"] }, "port": { "$ref": "#/definitions/port" }, "database": { "type": ["string", "null"], "minLength": 1 }, "pool": { "description": "Connection pool sizes, PER REPLICA. Upstream's db-pool-max-size default is 100; this chart ships 20, because two replicas at 100 ask a default PostgreSQL (max_connections = 100) for twice what it has.", "type": ["object", "null"], "additionalProperties": false, "properties": { "initialSize": { "type": ["integer", "null"], "minimum": 0 }, "minSize": { "type": ["integer", "null"], "minimum": 0 }, "maxSize": { "type": ["integer", "null"], "minimum": 1 } } } } }, "bootstrapAdmin": { "description": "The temporary bootstrap administrator. Switch this off once a permanent administrator exists - it also stops the per-start log line about the initial administrator not being created.", "type": ["object", "null"], "additionalProperties": false, "properties": { "enabled": { "type": "boolean" } } }, "metrics": { "description": "A Keycloak BUILD option (metrics-enabled). Off, because the management port is unauthenticated in this chart's configuration and metrics add realm and client names to that surface.", "type": ["object", "null"], "additionalProperties": false, "properties": { "enabled": { "type": "boolean" } } }, "features": { "description": "Additional Keycloak feature flags (--features), a BUILD option. A list rather than a comma-separated string, so two entries cannot arrive as one unknown feature name - which Keycloak rejects at build time, inside the init container. The pattern keeps a value out that could not be a feature name.", "type": ["array", "null"], "items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:._-]*$" } }, "cache": { "type": ["object", "null"], "additionalProperties": false, "properties": { "clusterName": { "description": "--cache-embedded-cluster-name. null means Keycloak's default, 'ISPN'. Read the comment in values.yaml before setting it: two releases sharing one database MERGE into one cluster unless the names differ, but upstream also warns that cache invalidation does not work between differently-named clusters.", "type": ["string", "null"], "minLength": 1 } } }, "ports": { "description": "May be null / empty: the chart then falls back to its defaults. The Deployment renders these as containerPorts and the Services address them by name, so both follow one source.", "type": ["object", "null"], "additionalProperties": false, "properties": { "http": { "$ref": "#/definitions/port" }, "management": { "description": "The management port. It carries the unauthenticated /health/* endpoints and optionally /metrics, and all three probes address it by name. It is deliberately in no Ingress.", "$ref": "#/definitions/port" }, "jgroups": { "description": "JGroups transport port. Declared as a containerPort for the record; jdbc-ping opens direct pod-to-pod connections on it.", "$ref": "#/definitions/port" } } }, "resources": { "description": "Resources of the SERVING container. The memory request is not only about scheduling here: the image sizes the JVM heap relative to the container limit (-XX:MaxRAMPercentage=70), so request x limitFactor decides how much memory the application believes it has.", "$ref": "#/definitions/resources" }, "build": { "description": "The build init container, which runs the one-shot Quarkus augmentation. Its resources are transient - it is gone before the serving container starts - but they count for scheduling, because a pod's effective request is the maximum of its init containers' and the sum of its containers'.", "type": ["object", "null"], "additionalProperties": false, "properties": { "resources": { "$ref": "#/definitions/resources" } } }, "livenessProbe": { "$ref": "#/definitions/probe" }, "readinessProbe": { "$ref": "#/definitions/probe" }, "startupProbe": { "$ref": "#/definitions/probe" }, "securityContext": { "$ref": "#/definitions/securityContext" }, "podDisruptionBudget": { "description": "The first PodDisruptionBudget in this repository. It is rendered only while the effective replica count is >= 2: minAvailable 1 with a single replica forbids every voluntary eviction, so a node drain would block forever on a pod nothing may move.", "type": ["object", "null"], "additionalProperties": false, "properties": { "enabled": { "type": "boolean" }, "minAvailable": { "type": "integer", "minimum": 1 } } }, "env": { "$ref": "#/definitions/env" } } }, "ingress": { "type": "object", "additionalProperties": false, "properties": { "enabled": { "description": "Gates the public Ingress object only (issue #93). Checked in both directions for this chart: ingress.domain is ALSO read as KC_HOSTNAME on the Deployment, so switching the Ingress off frees clusterIssuer but NOT domain - Keycloak refuses to start in production mode without a hostname.", "type": "boolean" }, "clusterIssuer": { "type": ["string", "null"] }, "domain": { "description": "The public hostname, a plain host - the chart renders it as the full URL https:// for KC_HOSTNAME, which upstream requires as soon as hostname-admin is in play. Required even with enabled: false.", "type": ["string", "null"] }, "className": { "type": ["string", "null"] }, "annotations": { "description": "Extra annotations for the public Ingress, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open.", "type": "object" }, "admin": { "description": "The administration console on a hostname of its own. NOT AN ACCESS CONTROL: hostname-admin decides which URLs the server generates; upstream states that it does not prevent reaching the Administration REST API through the public hostname, and that restricting that API is reverse-proxy work. The chart renders no deny rule for /admin.", "type": ["object", "null"], "additionalProperties": false, "properties": { "enabled": { "type": "boolean" }, "domain": { "description": "The admin hostname, a plain host - rendered as https:// for KC_HOSTNAME_ADMIN. Required while enabled.", "type": ["string", "null"] }, "className": { "description": "null falls back to ingress.className. An internal ingress class here keeps the console off the public controller, which is the useful half of the admin hostname.", "type": ["string", "null"] }, "annotations": { "description": "Extra annotations for the admin Ingress, merged with the cert-manager annotation. Free-form keys by nature, so this object stays open.", "type": "object" } } } } } }, "definitions": { "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. A BUILD option's variable name (KC_DB, KC_HEALTH_ENABLED, KC_METRICS_ENABLED, KC_FEATURES, ...) is refused by the template with a fail: on the serving container it would disagree with what the init container built, and Keycloak answers that by re-augmenting onto a read-only path.", "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, which deviate from the repo's usual 20000/1000 pair - the published image declares keycloak:x:1000:0 in /etc/passwd, puts that user in the ROOT GROUP and copies the whole install tree with --chown=1000:0, so uid 1000 / gid 0 is what the image expects and what makes the mounted build volume writable. Every container in the pod, both init containers included, uses the same container context: readOnlyRootFilesystem: true holds throughout, which is the reason the chart has two init containers rather than one writable one.", "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 securityContext of every container in the pod; 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 'management' for all three, and three DIFFERENT paths - /health/live for liveness (no database check), /health/ready for readiness (with one), /health/started for startup. Pointing liveness at /health or /health/ready would restart every pod of the cluster for as long as the database is away.", "type": ["object", "null"] } } }