Retrofits the repo-wide groundwork that #59 deliberately left out to keep the reviewed scope. Chart 2.0.0+up1.0.0 - a major, because seccompProfile changes runtime behaviour; appVersion stays the freely chosen marker 1.0.0. values.schema.json (#68). The largest schema in the repo: nine app blocks with probes, resources, security contexts, env and image. additionalProperties is false on every chart-owned level; the objects that go verbatim into the pod spec stay open. It rejects a mistyped app name, a mistyped key at any depth and ingress.domain - the key #59 removed, which until now would have been accepted in silence. Null safety (#99), both halves, because they cover different cases. The defaultedDict helper catches a key WITH a chart default, where Helm's coalescing deletes the null before the schema ever sees it; the widened ["object","null"] types catch a key WITHOUT one, where the null survives into validation. Measured on the old chart: radarr.securityContext: null rendered "securityContext: null" - the whole hardening gone - and securityContext.capabilities: null dropped the capability block entirely, so the container kept every default capability instead of dropping ALL. Both render green. The fallback table reproduces THIS chart's defaults including its deviations: the five linuxserver.io containers keep runAsNonRoot unset and a writable root filesystem so their s6 init can drop to PUID/PGID, flaresolverr keeps its numerically pinned uid, gluetun keeps NET_ADMIN, and recyclarr keeps its exec probes. Restoring the "clean" standard there would put the linuxserver containers back on the image's uid 911 - the failure calibre-web-automated hit. The sync between table and values.yaml is not left to discipline: rendering with every block of the silent class explicitly set to null is byte-identical to rendering the defaults, which is how a drift shows up. seccompProfile RuntimeDefault (#84) in the pod context, so it covers the init container too. Trivy's KSV-0104 drops from ten findings to none. NOTES.txt (#43) covering what actually bites: the pod is not Ready until the tunnel is up, the two fields the 1Password item needs, the warning when firewallOutboundSubnets is unset, the immutable volumeClaimTemplates, and the measured fact that imports do not hardlink across the media categories. Also, from point 5 of the plan: checksum/config does not apply - the chart has no ConfigMap - but checksum/secret-ref does, and was missing. Helm never sees the secret's contents, yet it does see which op:// item the OnePasswordItem points at, so repointing secrets.gluetunSecretRef now restarts the pod instead of leaving the old credentials running. README: the #43 status line was stale (it still claimed only homeassistant ships NOTES.txt), and the mandatory-icon rule now records freesailarr as a documented exception - #59 dropped the icon and did not amend the rule.
731 lines
19 KiB
JSON
731 lines
19 KiB
JSON
{
|
|
"$schema": "https://json-schema.org/draft-07/schema#",
|
|
"title": "freesailarr values",
|
|
"description": "Values schema (issue #68). additionalProperties is false on every chart-owned object level, so a key the chart does not read is rejected at render time instead of being silently ignored. The objects that go verbatim into the pod spec - probes, security contexts, free-form annotations, global - stay open, because the chart does not interpret their contents. The hardening, probe and resources blocks additionally accept null: an override may erase them, and the chart then falls back to its own defaults (issue #99) rather than rendering a container without hardening, without a probe or without resource accounting.",
|
|
"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": {
|
|
"type": "boolean"
|
|
},
|
|
"replicas": {
|
|
"type": "integer",
|
|
"minimum": 0
|
|
},
|
|
"secrets": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"gluetunSecretRef": {
|
|
"description": "op:// item path for the gluetun VPN credentials. The item must carry the fields wireguard-private-key and wireguard-addresses; the template reads exactly those names.",
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"puid": {
|
|
"description": "User the linuxserver.io containers drop to after their s6-overlay init, and runAsUser for seerr and recyclarr.",
|
|
"type": "integer"
|
|
},
|
|
"pgid": {
|
|
"type": "integer"
|
|
},
|
|
"timezone": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/podSecurityContextBlock"
|
|
},
|
|
"storage": {
|
|
"description": "StorageClass for the config volumeClaimTemplates. Deliberately NOT null-safe: an erased block yields an invalid claim template, which fails loudly, and issue #99 covers only the silent class.",
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"storageClass": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"media": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"downloads": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"existingClaim": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"subPath": {
|
|
"description": "Sub-directory of the claim to mount instead of its root. null means the claim root.",
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"movies": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"existingClaim": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"subPath": {
|
|
"description": "Sub-directory of the claim to mount instead of its root. null means the claim root.",
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"tv": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"existingClaim": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"subPath": {
|
|
"description": "Sub-directory of the claim to mount instead of its root. null means the claim root.",
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"ingress": {
|
|
"description": "Defaults for the per-app ingresses. There is deliberately no global domain - each app carries its own host.",
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"description": "Master switch; false suppresses every app ingress regardless of the per-app toggles.",
|
|
"type": "boolean"
|
|
},
|
|
"clusterIssuer": {
|
|
"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.",
|
|
"type": "object"
|
|
}
|
|
}
|
|
},
|
|
"gluetun": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"vpn": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"serviceProvider": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"type": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"serverCities": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"serverCountries": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"dnsUpstreamResolvers": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"firewallOutboundSubnets": {
|
|
"description": "Comma-separated CIDRs the gluetun firewall allows outbound outside the tunnel. Set to the cluster pod and service CIDRs.",
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"storage": {
|
|
"$ref": "#/definitions/appStorage"
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"qbittorrent": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"ingress": {
|
|
"$ref": "#/definitions/appIngress"
|
|
},
|
|
"storage": {
|
|
"$ref": "#/definitions/appStorage"
|
|
},
|
|
"networkInterface": {
|
|
"description": "Interface the BitTorrent session binds to (gluetun's tun device). null or \"\" removes the init container that seeds it and leaves the binding unset - which lets libtorrent announce past the tunnel.",
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"configInit": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"prowlarr": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"ingress": {
|
|
"$ref": "#/definitions/appIngress"
|
|
},
|
|
"storage": {
|
|
"$ref": "#/definitions/appStorage"
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"flaresolverr": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"radarr": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"ingress": {
|
|
"$ref": "#/definitions/appIngress"
|
|
},
|
|
"storage": {
|
|
"$ref": "#/definitions/appStorage"
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"recyclarr": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"storage": {
|
|
"$ref": "#/definitions/appStorage"
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"sonarr": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"ingress": {
|
|
"$ref": "#/definitions/appIngress"
|
|
},
|
|
"storage": {
|
|
"$ref": "#/definitions/appStorage"
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"seerr": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"ingress": {
|
|
"$ref": "#/definitions/appIngress"
|
|
},
|
|
"storage": {
|
|
"$ref": "#/definitions/appStorage"
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
},
|
|
"bazarr": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"image": {
|
|
"$ref": "#/definitions/image"
|
|
},
|
|
"ingress": {
|
|
"$ref": "#/definitions/appIngress"
|
|
},
|
|
"storage": {
|
|
"$ref": "#/definitions/appStorage"
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/containerSecurityContext"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"definitions": {
|
|
"image": {
|
|
"description": "Every image in this chart is pinned here, gluetun included; no tag comes from appVersion. This is also what puts them all under Renovate's helm-values manager, which reads exactly these two keys.",
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"repository": {
|
|
"type": "string"
|
|
},
|
|
"tag": {
|
|
"type": [
|
|
"string",
|
|
"number"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"appIngress": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"host": {
|
|
"description": "Required while this app's ingress is enabled, and never demanded when it is off.",
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"clusterIssuer": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"className": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
},
|
|
"annotations": {
|
|
"description": "Overrides the global ingress annotations for this app; free-form, so kept open.",
|
|
"type": [
|
|
"object",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"appStorage": {
|
|
"description": "Size of this app's config volumeClaimTemplate. Not null-safe on purpose: an erased size yields an invalid claim template and fails loudly.",
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"size": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"resources": {
|
|
"description": "May be null / empty: the chart then falls back to this container's own default requests and computed limits (issue #99).",
|
|
"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": {
|
|
"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"
|
|
}
|
|
}
|
|
},
|
|
"podSecurityContextBlock": {
|
|
"description": "May be null / empty at any level: the chart then falls back to its pod hardening defaults (issue #99), seccompProfile included.",
|
|
"type": [
|
|
"object",
|
|
"null"
|
|
],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"pod": {
|
|
"description": "Merged onto the chart's pod hardening defaults; kept open because the chart does not interpret the individual fields.",
|
|
"type": [
|
|
"object",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"containerSecurityContext": {
|
|
"description": "Merged onto THIS container's hardening defaults; kept open because the chart does not interpret the individual fields. May be null / empty, in which case those defaults apply (issue #99). The fallbacks reproduce this chart's deliberate deviations - the linuxserver.io containers start as root and keep a writable root filesystem so their s6 init can drop to PUID/PGID, and restoring the 'clean' standard there would leave them on the image's uid 911.",
|
|
"type": [
|
|
"object",
|
|
"null"
|
|
]
|
|
},
|
|
"probe": {
|
|
"description": "Merged onto this container's probe defaults; 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 their context - path and port, and recyclarr's exec command, since it serves no HTTP port.",
|
|
"type": [
|
|
"object",
|
|
"null"
|
|
]
|
|
}
|
|
}
|
|
}
|