Files
cozystack/packages/extra/monitoring/values.schema.json
IvanHunters c31a9f8ec3 fix(monitoring): promote oidc.users[].role to required + regenerate schema
The `OIDCUser` typedef in `packages/extra/monitoring/values.yaml`
declared only `@field {string} email`; `cozyvalues-gen` regenerated
`values.schema.json`, `README.md`, and the mirrored `openAPISchema`
on the `Monitoring` ApplicationDefinition WITHOUT a `role` field.
Consequences:

- The `@enum OIDCRole` declaration was orphan — referenced from
  nowhere, so the schema's items object had `required: ["email"]`
  and one property.
- A dashboard-driven CR would offer a form without any role picker;
  a schema-conformant `{email}` submission would render into the
  users-Job with `role: null`, `jq --raw-output` yields the string
  `"null"`, the `POST /api/orgs/1/users` body carries
  `"role":"null"`, Grafana returns HTTP 400, and the post-install
  hook exhausts `backoffLimit: 6` — HelmRelease never becomes Ready.
- Unit + e2e tests all hardcoded `role: Admin` / `role: Viewer`, so
  the contract mismatch never surfaced in CI.

Fix:

- Add `@field {OIDCRole} role - ...` to the `OIDCUser` typedef in
  `packages/extra/monitoring/values.yaml` so cozyvalues-gen materialises
  the field with enum + required.
- Regenerate `values.schema.json`, `README.md`, and mirrored
  `openAPISchema` via `make generate`.
- Add `monitoring.oidc.assertUsersRoleShape` template-level guard
  (same shape as `assertUsersEmailShape`) so a stale-schema CR or a
  direct-helm invocation fails at render time with a legible error
  instead of at hook execution with a Grafana 400.
- Consolidate all `spec.oidc.users` guards into a single
  `monitoring.oidc.assertUsersPrologue` include and call it once from
  `oidc-users-job.yaml`, ordering the incompat check
  (`assertCustomSecretRefUsersEmpty`) before the per-entry role /
  email regexes so operators see the more actionable message first.
  Drop the duplicate include from `grafana.yaml` (Job template renders
  in every mode anyway).
- Two new helm-unittest cases lock the regression:
  `oidc.users entry missing role fails the render` and
  `oidc.users entry with unknown role fails the render`.
- Point the previous `secretRef + users` incompat unit test at the
  users-Job template since that's where the prologue now fires.
- `docs/oidc-grafana.md`: correct the failed-hook post-mortem window
  claim — with `ttlSecondsAfterFinished: 3600` the Kubernetes TTL
  controller reaps the Job + Pod 1h after the terminal condition,
  not "until the next helm-upgrade".

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-07-15 15:31:45 +03:00

672 lines
24 KiB
JSON

{
"title": "Chart Values",
"type": "object",
"properties": {
"host": {
"description": "The hostname used to access Grafana externally (defaults to 'grafana' subdomain for the tenant host).",
"type": "string",
"default": ""
},
"metricsStorages": {
"description": "Configuration of metrics storage instances.",
"type": "array",
"default": [
{
"deduplicationInterval": "15s",
"name": "shortterm",
"retentionPeriod": "3d",
"storage": "10Gi",
"storageClassName": ""
},
{
"deduplicationInterval": "5m",
"name": "longterm",
"retentionPeriod": "14d",
"storage": "10Gi",
"storageClassName": ""
}
],
"items": {
"type": "object",
"required": [
"deduplicationInterval",
"name",
"retentionPeriod",
"storage"
],
"properties": {
"deduplicationInterval": {
"description": "Deduplication interval for metrics.",
"type": "string"
},
"name": {
"description": "Name of the storage instance.",
"type": "string"
},
"retentionPeriod": {
"description": "Retention period for metrics.",
"type": "string"
},
"storage": {
"description": "Persistent volume size.",
"type": "string",
"default": "10Gi"
},
"storageClassName": {
"description": "StorageClass used for the data.",
"type": "string"
},
"vminsert": {
"description": "Configuration for vminsert.",
"type": "object",
"properties": {
"maxAllowed": {
"description": "Maximum allowed resources.",
"type": "object",
"properties": {
"cpu": {
"description": "CPU limit.",
"default": 1,
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "Memory limit.",
"default": "1Gi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"minAllowed": {
"description": "Minimum guaranteed resources.",
"type": "object",
"properties": {
"cpu": {
"description": "CPU request.",
"default": "100m",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "Memory request.",
"default": "256Mi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
}
}
},
"vmselect": {
"description": "Configuration for vmselect.",
"type": "object",
"properties": {
"maxAllowed": {
"description": "Maximum allowed resources.",
"type": "object",
"properties": {
"cpu": {
"description": "CPU limit.",
"default": 1,
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "Memory limit.",
"default": "1Gi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"minAllowed": {
"description": "Minimum guaranteed resources.",
"type": "object",
"properties": {
"cpu": {
"description": "CPU request.",
"default": "100m",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "Memory request.",
"default": "256Mi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
}
}
},
"vmstorage": {
"description": "Configuration for vmstorage.",
"type": "object",
"properties": {
"maxAllowed": {
"description": "Maximum allowed resources.",
"type": "object",
"properties": {
"cpu": {
"description": "CPU limit.",
"default": 1,
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "Memory limit.",
"default": "1Gi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"minAllowed": {
"description": "Minimum guaranteed resources.",
"type": "object",
"properties": {
"cpu": {
"description": "CPU request.",
"default": "100m",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "Memory request.",
"default": "256Mi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
}
}
}
}
}
},
"logsStorages": {
"description": "Configuration of logs storage instances.",
"type": "array",
"default": [
{
"name": "generic",
"retentionPeriod": "1",
"storage": "10Gi",
"storageClassName": "replicated"
}
],
"items": {
"type": "object",
"required": [
"name",
"retentionPeriod",
"storage",
"storageClassName"
],
"properties": {
"name": {
"description": "Name of the storage instance.",
"type": "string"
},
"retentionPeriod": {
"description": "Retention period for logs.",
"type": "string",
"default": "1"
},
"storage": {
"description": "Persistent volume size.",
"type": "string",
"default": "10Gi"
},
"storageClassName": {
"description": "StorageClass used to store the data.",
"type": "string",
"default": "replicated"
}
}
}
},
"alerta": {
"description": "Configuration for the Alerta service.",
"type": "object",
"default": {},
"properties": {
"alerts": {
"description": "Alert routing configuration.",
"type": "object",
"default": {},
"properties": {
"slack": {
"description": "Configuration for Slack alerts.",
"type": "object",
"default": {},
"required": [
"url"
],
"properties": {
"disabledSeverity": {
"description": "List of severities without alerts (e.g. [\"informational\",\"warning\"]).",
"type": "array",
"default": [],
"items": {
"type": "string"
}
},
"url": {
"description": "Configuration uri for Slack alerts.",
"type": "string",
"default": ""
}
}
},
"telegram": {
"description": "Configuration for Telegram alerts.",
"type": "object",
"default": {},
"required": [
"chatID",
"token"
],
"properties": {
"chatID": {
"description": "Telegram chat ID(s), separated by commas.",
"type": "string",
"default": ""
},
"disabledSeverity": {
"description": "List of severities without alerts (e.g. [\"informational\",\"warning\"]).",
"type": "array",
"default": [],
"items": {
"type": "string"
}
},
"token": {
"description": "Telegram bot token.",
"type": "string",
"default": ""
}
}
}
}
},
"resources": {
"description": "Resource configuration.",
"type": "object",
"default": {},
"properties": {
"limits": {
"description": "Resource limits.",
"type": "object",
"default": {},
"properties": {
"cpu": {
"description": "CPU limit.",
"default": 1,
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "Memory limit.",
"default": "1Gi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"requests": {
"description": "Resource requests.",
"type": "object",
"default": {},
"properties": {
"cpu": {
"description": "CPU request.",
"default": "100m",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "Memory request.",
"default": "256Mi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
}
}
},
"storage": {
"description": "Persistent volume size for the database.",
"type": "string",
"default": "10Gi"
},
"storageClassName": {
"description": "StorageClass used for the database.",
"type": "string",
"default": ""
}
}
},
"grafana": {
"description": "Configuration for Grafana.",
"type": "object",
"default": {},
"properties": {
"db": {
"description": "Database configuration.",
"type": "object",
"default": {},
"properties": {
"size": {
"description": "Persistent volume size for the database.",
"type": "string",
"default": "10Gi"
}
}
},
"resources": {
"description": "Resource configuration.",
"type": "object",
"default": {},
"properties": {
"limits": {
"description": "Resource limits.",
"type": "object",
"default": {},
"properties": {
"cpu": {
"description": "CPU limit.",
"default": 1,
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "Memory limit.",
"default": "1Gi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"requests": {
"description": "Resource requests.",
"type": "object",
"default": {},
"properties": {
"cpu": {
"description": "CPU request.",
"default": "100m",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "Memory request.",
"default": "256Mi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
}
}
}
}
},
"vmagent": {
"description": "Configuration for VictoriaMetrics Agent.",
"type": "object",
"default": {},
"properties": {
"externalLabels": {
"description": "External labels applied to all metrics.",
"type": "object",
"default": {
"cluster": "cozystack"
}
},
"remoteWrite": {
"description": "Remote write configuration.",
"type": "object",
"default": {
"urls": [
"http://vminsert-shortterm:8480/insert/0/prometheus",
"http://vminsert-longterm:8480/insert/0/prometheus"
]
}
}
}
},
"oidc": {
"description": "OIDC authentication for the Grafana instance. See docs/oidc-grafana.md for the operator guide.",
"type": "object",
"default": {},
"required": [
"mode"
],
"properties": {
"customConfig": {
"description": "Tenant-supplied `[auth.generic_oauth]` payload; consumed only when `mode: CustomConfig`.",
"type": "object",
"default": {},
"properties": {
"config": {
"description": "Inline map of `[auth.generic_oauth]` keys. Written verbatim into the Grafana CR `spec.config`. Mutually exclusive with `secretRef.name`.",
"type": "object",
"default": {}
},
"secretRef": {
"description": "Reference to a Secret holding an `auth.ini` fragment; mounted under `/etc/grafana/oidc` and loaded via `GF_PATHS_CUSTOM_INI`.",
"type": "object",
"default": {},
"properties": {
"name": {
"description": "Name of an existing Secret in the release namespace whose `auth.ini` key holds a ready-made `[auth.generic_oauth]` block. Mutually exclusive with `customConfig.config`.",
"type": "string",
"default": ""
}
}
}
}
},
"mode": {
"description": "Identity mode. `None`: no OIDC, only the `grafana-admin-password` Secret works. `System`: trust the platform `cozy` realm via a per-instance confidential client and audience binding; zero-config default. `CustomConfig`: trust a tenant-supplied issuer directly (BYO); `cozy` is not in the path.",
"type": "string",
"default": "None",
"enum": [
"None",
"System",
"CustomConfig"
]
},
"users": {
"description": "Users granted access to this Grafana instance's org. Each entry is reconciled into Grafana org membership + role by a chart-owned post-install/post-upgrade Job. Works for both `System` and `CustomConfig` modes; ignored when `mode: None`. Users not listed here get nothing (no default org role from group mapping).",
"type": "array",
"default": [],
"items": {
"type": "object",
"required": [
"email",
"role"
],
"properties": {
"email": {
"description": "Email address matched against the `email` claim from the issuer. The chart pre-provisions a Grafana account with this email so the users-Job can set the org role before the operator's first login (`oauth_allow_insecure_email_lookup = true` glues the two together). Must match a conservative pattern: local part is `[A-Za-z0-9._%+-]+`, domain is `[A-Za-z0-9.-]+\\.[A-Za-z]{2,}`. Whitespace, quoted literals, and unusual URL-reserved characters are rejected on schema level so the reconcile-Job's shell handling never has to defend against malformed input.",
"type": "string"
},
"role": {
"description": "Grafana org role to bind: `Admin`, `Editor`, or `Viewer`. Applied to the release's Grafana org (`Main Org.`); server-level `GrafanaAdmin` promotion is out of scope for Phase 1. Required — the users-Job POSTs `{loginOrEmail, role}` to `/api/orgs/1/users` and PATCHes `{role}` to converge across re-runs; omitting the field would render `role: \"null\"` into the body and fail with HTTP 400.",
"type": "string",
"enum": [
"Admin",
"Editor",
"Viewer"
]
}
}
}
}
}
}
}
}