Two corrections found while verifying the first CI run, not from the plan. THE DEFAULT THAT MADE THE READINESS PROBE TOOTHLESS. Keycloak's DATABASE health check is the one row of upstream's "Available Checks" table whose "Requires Metrics" column says yes; Cluster, Graceful shutdown and Keycloak Initialized say no. So with metrics-enabled=false - this chart's default until now - /health/ready answers 200 while the database is gone, and the pod keeps taking logins it cannot serve. The probe path was right and the aggregate behind it contained nothing external, which is the urlaubsverwaltung defect (#218) in a new hiding place: not a health group this time, but an option named for observability, where nobody looks when reviewing a probe. keycloak.metrics.enabled therefore defaults to TRUE, and its comment argues about readiness rather than about scraping. The price is stated at the value: /metrics then answers on the management port, unauthenticated, carrying realm names, client ids and login counters - on a port that is ClusterIP-only and in no Ingress. Switching it off is now documented as a decision about whether readiness can fail at all. The CI fixture keeps it on and says why, so a green run really does touch the database. THE STARTUP BUDGET NOW CARRIES ITS MEASUREMENT. From the install-test of PR #87 on the Gitea docker runner (k3s v1.35.5, two replicas, empty database, image pulled during the run): 71.8 seconds from `helm install` to BOTH pods Ready - first at ~70.8s, second one second later, 0 restarts. The 600s budget stays, with the reason for the headroom written down: everything in that run was the cheap case, and a first install against a real database serializes the second replica behind the first's Liquibase migration lock instead of letting it arrive a second later. AND AN OVERCLAIM OF MY OWN, REMOVED. ci/keycloak/test-values.yaml said a green run proves jdbc-ping found the second replica. It does not: readiness reports one node's own health, so a node that never found its peer is Ready, healthy and alone - and two such nodes are two islands that each believe they are the cluster. What the test does rule out is the louder half, a replica that cannot start or cannot share the database. The Infinispan view has to be read out of the log, which is what step 2 of NOTES.txt tells the operator to do, now with the note that no probe and no install-test replaces that step. Measurement point 4 of the plan therefore stays open. Part of #83
271 lines
16 KiB
JSON
271 lines
16 KiB
JSON
{
|
|
"$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). ON, and not for the metrics: upstream's table of health checks has a 'Requires Metrics' column, and the DATABASE check is the row that says yes. With this false, /health/ready still answers 200 while the database is gone - the urlaubsverwaltung shape (#218), with the dependency hidden behind an option whose name says observability. The price is that /metrics then answers on the management port, unauthenticated, carrying realm and client names - that port is ClusterIP-only and in no Ingress. Switching this off is a decision about whether readiness can fail.",
|
|
"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://<domain> 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://<domain> 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"]
|
|
}
|
|
}
|
|
}
|