Files
cozystack/packages/extra/monitoring/values.schema.json
T
Alexey Artamonov 60b1b05434 feat(monitoring): add per-tenant OTLP collector for distributed tracing
Applications need one in-namespace OTLP endpoint that also enforces
sampling and redaction before spans reach the VictoriaTraces backend.
No OpenTelemetry operator ships with the platform, so the collector is
a plain Deployment, ConfigMap and Service ('otel-traces', gRPC 4317,
HTTP 4318) in the tenant namespace; app-to-collector traffic therefore
never leaves the tenant, and isolation comes from the existing tenant
network policies.

The collector renders only when it is enabled and a tracingStorages
backend is configured. Tracing is off by default, and an empty backend
list is a clean off-state rather than a render failure; an app exporter
that finds no Service simply retries. It writes to the first backend
only and rejects a list with several, instead of silently dropping
spans for the others.

redactAttributes is the PII and secret control point, so its keys are
rendered quoted and empty keys are dropped: an unquoted key with YAML
structural characters, or an empty one, makes the collector fail at
startup, and a key YAML re-types (numbers, booleans, null) silently
escapes redaction. replicas has a lower bound of 1 so a bad value fails
schema validation instead of the whole release at apply time; a zeroed
collector is expressed with enabled: false.

The collector gets a WorkloadMonitor and a dashboard-resources Role
entry under the same render gate, so it is part of tenant workload
accounting like every other workload in the chart.

Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-09-25 13:35:34 +03:00

845 lines
31 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"
}
}
}
},
"tracingStorages": {
"description": "Configuration of traces storage instances. Empty by default (tracing off): no backend and no trace collector are rendered. Adding an entry provisions a VictoriaTraces backend and, while tracingCollector.enabled is true, deploys the OTLP collector in front of it.",
"type": "array",
"default": [],
"items": {
"type": "object",
"required": [
"mode",
"name",
"retentionPeriod",
"storage",
"storageClassName"
],
"properties": {
"mode": {
"description": "Deployment mode: \"cluster\" renders a VTCluster; \"single\" renders a single-binary VTSingle for edge/dev/small clusters. Treat as immutable once a backend exists: mode selects which Kind is rendered under the same name, so changing it on a running backend re-identifies the object rather than reconfiguring it. Switching from \"single\" to \"cluster\" prunes the VTSingle and the operator garbage-collects its PVC (stored traces are lost); switching from \"cluster\" to \"single\" (or removing the entry) leaves the vtstorage PVCs Bound and billed until the whole Monitoring release is deleted. The API cannot guard this (transition rules are not available on an array-item field), so this note is the only guard: migrate deliberately, accepting the loss.",
"type": "string",
"default": "cluster",
"enum": [
"cluster",
"single"
]
},
"name": {
"description": "Name of the storage instance: a lowercase RFC 1123 label of at most 42 characters. Treat as immutable once a backend exists: the VTCluster/VTSingle, its Services, PVCs and Grafana datasource are all named after it, so renaming an entry deploys a new, empty backend and removes the old one, the same way changing mode does (single mode loses the old PVC, cluster mode leaves the old vtstorage PVCs Bound until the whole Monitoring release is deleted).",
"type": "string"
},
"retentionDiskUsageBytes": {
"description": "Optional disk-usage cap as a VictoriaMetrics BytesString (e.g. \"8GB\" or \"8GiB\"), bounding disk use on top of the age-based retentionPeriod. Uses byte-suffix grammar (kb/mb/gb/tb, KiB/MiB/GiB/TiB), NOT the Kubernetes-quantity grammar of the sibling storage field, so \"8Gi\" is rejected by the operator. In cluster mode the cap is per vtstorage replica (replicaCount 2), so the effective total is twice the value.",
"type": "string"
},
"retentionPeriod": {
"description": "Retention period for traces.",
"type": "string",
"default": "14d"
},
"storage": {
"description": "Persistent volume size.",
"default": "10Gi",
"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
},
"storageClassName": {
"description": "StorageClass used to store the data.",
"type": "string",
"default": "replicated"
}
}
}
},
"tracingCollector": {
"description": "OpenTelemetry Collector for trace ingestion.",
"type": "object",
"default": {},
"required": [
"enabled",
"replicas",
"samplingPercentage"
],
"properties": {
"enabled": {
"description": "Deploy the OTLP collector. When disabled, applications must send OTLP straight to the traces backend (no central sampling/redaction). The collector fronts a single backend: while it is enabled, tracingStorages must hold at most one entry, and adding a second one fails the whole Monitoring render until the extra entry is removed or the collector is disabled.",
"type": "boolean",
"default": true
},
"redactAttributes": {
"description": "Span attribute keys to drop before export (the PII/secret control point, e.g. db.statement).",
"type": "array",
"default": [],
"items": {
"type": "string"
}
},
"replicas": {
"description": "Number of collector replicas. Head sampling is trace-id-deterministic, so replicas\u003e1 are safe and remove the single-ingest-path SPOF.",
"type": "integer",
"default": 1,
"minimum": 1
},
"resources": {
"description": "Resource configuration for the collector.",
"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
}
}
}
}
},
"samplingPercentage": {
"description": "Head-sampling percentage applied at the collector before export.",
"type": "integer",
"default": 10,
"maximum": 100,
"minimum": 0
}
}
},
"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"
]
}
}
}
}
}
}
}
}