feat(mariadb): let the external endpoint wear a reserved address

Which Service actually receives the address depends on the replica count: the
operator publishes the cluster Service for a single replica and primaryService
once there are more, and the chart already branches on exactly that. The claim
annotation follows the same branch, so a tenant writes one entry either way.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
This commit is contained in:
Andrei Kvapil committed 2026-09-11 20:17:11 +02:00
1 parent 1cef6f27b4
commit dca7a5fd0d
7 files changed
+170 -12

No files matched your search

+10
View File
@@ -36,6 +36,9 @@ type ConfigSpec struct {
// Enable external access from outside the cluster.
// +kubebuilder:default:=false
External bool `json:"external"`
// Reserved addresses for the endpoints `external: true` publishes. Each entry pins one endpoint to an address held by an IPAddressClaim, so the address survives a rebuild of this application. Empty lets the load balancer assign any free address, as before.
// +kubebuilder:default:={}
ExternalIPs []ExternalIP `json:"externalIPs,omitempty"`
// TLS configuration. Selects who issues the certificates and whether plaintext is refused; TLS itself is always served.
// +kubebuilder:default:={}
Tls TLS `json:"tls,omitempty"`
@@ -92,6 +95,13 @@ type DatabaseRoles struct {
Readonly []string `json:"readonly,omitempty"`
}
type ExternalIP struct {
// Name of an IPAddressClaim in this namespace whose address the endpoint should wear. Until the claim binds, the endpoint keeps whatever address the load balancer assigned.
Claim string `json:"claim"`
// Which external endpoint to pin. `primary` is the read-write endpoint, the endpoint `external: true` publishes, and is the default.
Target string `json:"target,omitempty"`
}
type Resources struct {
// CPU available to each replica.
Cpu resource.Quantity `json:"cpu,omitempty"`
+13 -10
View File
@@ -173,16 +173,19 @@ Uninstalling the instance does this for you; only the turn-it-off-and-keep-runni
### Common parameters
| Name | Description | Type | Value |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ---------- | --------- |
| `replicas` | Number of MariaDB replicas. | `int` | `2` |
| `resources` | Explicit CPU and memory configuration for each MariaDB replica. When omitted, the preset defined in `resourcesPreset` is applied. | `object` | `{}` |
| `resources.cpu` | CPU available to each replica. | `quantity` | `""` |
| `resources.memory` | Memory (RAM) available to each replica. | `quantity` | `""` |
| `resourcesPreset` | Default sizing preset used when `resources` is omitted. | `string` | `t1.nano` |
| `size` | Persistent Volume Claim size available for application data. | `quantity` | `10Gi` |
| `storageClass` | StorageClass used to store the data. | `string` | `""` |
| `external` | Enable external access from outside the cluster. | `bool` | `false` |
| Name | Description | Type | Value |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | --------- |
| `replicas` | Number of MariaDB replicas. | `int` | `2` |
| `resources` | Explicit CPU and memory configuration for each MariaDB replica. When omitted, the preset defined in `resourcesPreset` is applied. | `object` | `{}` |
| `resources.cpu` | CPU available to each replica. | `quantity` | `""` |
| `resources.memory` | Memory (RAM) available to each replica. | `quantity` | `""` |
| `resourcesPreset` | Default sizing preset used when `resources` is omitted. | `string` | `t1.nano` |
| `size` | Persistent Volume Claim size available for application data. | `quantity` | `10Gi` |
| `storageClass` | StorageClass used to store the data. | `string` | `""` |
| `external` | Enable external access from outside the cluster. | `bool` | `false` |
| `externalIPs` | Reserved addresses for the endpoints `external: true` publishes. Each entry pins one endpoint to an address held by an IPAddressClaim, so the address survives a rebuild of this application. Empty lets the load balancer assign any free address, as before. | `[]object` | `[]` |
| `externalIPs[i].claim` | Name of an IPAddressClaim in this namespace whose address the endpoint should wear. Until the claim binds, the endpoint keeps whatever address the load balancer assigned. | `string` | `""` |
| `externalIPs[i].target` | Which external endpoint to pin. `primary` is the read-write endpoint, the endpoint `external: true` publishes, and is the default. | `string` | `""` |
### TLS parameters
@@ -1,4 +1,8 @@
{{- $certManagerIssued := (include "mariadb.tls.certManagerIssued" .) | eq "true" -}}
{{- /* Validated unconditionally: externalIPs without external: true is a
configuration mistake worth reporting, and this file is the only place in
the chart that consumes the value. */ -}}
{{- $externalIPs := include "cozy-lib.externalIP.claims" (list .Values.externalIPs .Values.external "primary" (list "primary") $) | fromYaml -}}
---
apiVersion: k8s.mariadb.com/v1alpha1
kind: MariaDB
@@ -96,6 +100,16 @@ spec:
metadata:
labels:
app.kubernetes.io/instance: {{ $.Release.Name }}
{{- /* With a single replica the operator publishes the cluster Service
itself, so the primary's reserved address belongs here. With more
replicas that Service stays ClusterIP and primaryService below is the
one that gets the address. */}}
{{- if eq (int .Values.replicas) 1 }}
{{- with (include "cozy-lib.externalIP.annotations" (list $externalIPs "primary" $)) }}
annotations:
{{- . | nindent 8 }}
{{- end }}
{{- end }}
{{- if and .Values.external (eq (int .Values.replicas) 1) }}
type: LoadBalancer
{{- end }}
@@ -110,6 +124,11 @@ spec:
{{- if and .Values.external (gt (int .Values.replicas) 1) }}
primaryService:
type: LoadBalancer
{{- with (include "cozy-lib.externalIP.annotations" (list $externalIPs "primary" $)) }}
metadata:
annotations:
{{- . | nindent 8 }}
{{- end }}
{{- end }}
#secondaryService:
@@ -0,0 +1,95 @@
suite: reserved external addresses (externalIPs)
# The chart's only job here is to turn an externalIPs entry into the address
# substrate's consumption annotation on whichever operator-owned Service
# actually receives the load-balancer address, and to refuse a value that would
# otherwise be silently ignored. Which Service that is depends on the replica
# count, which is the only thing worth locking down here.
templates:
- templates/mariadb.yaml
release:
name: mariadb-test
namespace: tenant-test
tests:
- it: "external without externalIPs: no annotation, so the load balancer keeps auto-assigning"
set:
external: true
replicas: 1
asserts:
- notExists:
path: spec.service.metadata.annotations
documentSelector:
path: kind
value: MariaDB
- it: "single replica: the address lands on the cluster Service the operator publishes"
set:
external: true
replicas: 1
externalIPs:
- claim: reserved-address
asserts:
- equal:
path: spec.service.metadata.annotations["local.sdn.cozystack.io/ip-address-claim"]
value: reserved-address
documentSelector:
path: kind
value: MariaDB
- notExists:
path: spec.primaryService
documentSelector:
path: kind
value: MariaDB
- it: "replicated: the address lands on primaryService, which is the one published"
set:
external: true
replicas: 3
externalIPs:
- claim: reserved-address
target: primary
asserts:
- equal:
path: spec.primaryService.metadata.annotations["local.sdn.cozystack.io/ip-address-claim"]
value: reserved-address
documentSelector:
path: kind
value: MariaDB
- notExists:
path: spec.service.metadata.annotations
documentSelector:
path: kind
value: MariaDB
- it: "externalIPs without external is refused rather than ignored"
set:
external: false
externalIPs:
- claim: reserved-address
asserts:
- failedTemplate:
errorMessage: "externalIPs requires external: true — there is no external endpoint to pin an address to otherwise"
- it: "a target this chart does not publish is refused, and the message names the ones it does"
set:
external: true
externalIPs:
- claim: reserved-address
target: secondary
asserts:
- failedTemplate:
errorMessage: 'externalIPs[0] targets "secondary", which this application does not publish; valid targets: primary'
- it: "two entries for the same endpoint are refused"
set:
external: true
externalIPs:
- claim: first
- claim: second
target: primary
asserts:
- failedTemplate:
errorMessage: 'externalIPs[1] targets "primary" a second time: one endpoint wears one address, so use one entry per target'
+21
View File
@@ -127,6 +127,27 @@
"type": "boolean",
"default": false
},
"externalIPs": {
"description": "Reserved addresses for the endpoints `external: true` publishes. Each entry pins one endpoint to an address held by an IPAddressClaim, so the address survives a rebuild of this application. Empty lets the load balancer assign any free address, as before.",
"type": "array",
"default": [],
"items": {
"type": "object",
"required": [
"claim"
],
"properties": {
"claim": {
"description": "Name of an IPAddressClaim in this namespace whose address the endpoint should wear. Until the claim binds, the endpoint keeps whatever address the load balancer assigned.",
"type": "string"
},
"target": {
"description": "Which external endpoint to pin. `primary` is the read-write endpoint, the endpoint `external: true` publishes, and is the default.",
"type": "string"
}
}
}
},
"tls": {
"description": "TLS configuration. Selects who issues the certificates and whether plaintext is refused; TLS itself is always served.",
"type": "object",
+10
View File
@@ -75,6 +75,16 @@ storageClass: ""
## @param {bool} external - Enable external access from outside the cluster.
external: false
## @typedef {struct} ExternalIP - A reserved external address for one of the application's external endpoints.
## @field {string} claim - Name of an IPAddressClaim in this namespace whose address the endpoint should wear. Until the claim binds, the endpoint keeps whatever address the load balancer assigned.
## @field {string} [target] - Which external endpoint to pin. `primary` is the read-write endpoint, the endpoint `external: true` publishes, and is the default.
## @param {[]ExternalIP} externalIPs - Reserved addresses for the endpoints `external: true` publishes. Each entry pins one endpoint to an address held by an IPAddressClaim, so the address survives a rebuild of this application. Empty lets the load balancer assign any free address, as before.
externalIPs: []
## Example:
## externalIPs:
## - claim: mariadb-primary-address
##
## @section TLS parameters
##
+2 -2
View File
@@ -8,7 +8,7 @@ spec:
plural: mariadbs
singular: mariadb
openAPISchema: |-
{"title":"Chart Values","type":"object","properties":{"replicas":{"description":"Number of MariaDB replicas.","type":"integer","default":2},"resources":{"description":"Explicit CPU and memory configuration for each MariaDB replica. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each replica.","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 (RAM) available to each replica.","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}}},"resourcesPreset":{"description":"Default sizing preset used when `resources` is omitted.","type":"string","default":"t1.nano","enum":["t1.nano","t1.micro","t1.small","t1.medium","t1.large","t1.xlarge","t1.2xlarge","t1.4xlarge","c1.nano","c1.micro","c1.small","c1.medium","c1.large","c1.xlarge","c1.2xlarge","c1.4xlarge","s1.nano","s1.micro","s1.small","s1.medium","s1.large","s1.xlarge","s1.2xlarge","s1.4xlarge","u1.nano","u1.micro","u1.small","u1.medium","u1.large","u1.xlarge","u1.2xlarge","u1.4xlarge","m1.nano","m1.micro","m1.small","m1.medium","m1.large","m1.xlarge","m1.2xlarge","m1.4xlarge","nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume Claim size available for application data.","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},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":"","x-kubernetes-validations":[{"rule":"self == oldSelf","message":"storageClass is immutable"}],"x-cozystack-options":{"source":"storageclass"}},"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"tls":{"description":"TLS configuration. Selects who issues the certificates and whether plaintext is refused; TLS itself is always served.","type":"object","default":{},"properties":{"issuer":{"description":"Who issues the server certificate: `operator` uses the CA mariadb-operator manages itself, `cert-manager` gives the instance its own CA and is what covers the external hostname. Named for the issuer because TLS is served under both, unlike the similarly-spelled `tls.enabled` in some other charts, which does switch TLS on and off.","type":"string","default":"operator","enum":["operator","cert-manager"]},"required":{"description":"Refuse plaintext connections (sets MariaDB require_secure_transport=ON). Applies under either issuer. Opt-in: turn it on once every client connects over TLS, since it disconnects those that do not.","type":"boolean"}}},"version":{"description":"MariaDB major.minor version to deploy","type":"string","default":"v11.8","enum":["v11.8","v11.4","v10.11","v10.6"]},"users":{"description":"Users configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","required":["maxUserConnections","password"],"properties":{"maxUserConnections":{"description":"Maximum number of connections.","type":"integer"},"password":{"description":"Password for the user.","type":"string"}}}},"databases":{"description":"Databases configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","properties":{"roles":{"description":"Roles assigned to users.","type":"object","properties":{"admin":{"description":"List of users with admin privileges.","type":"array","items":{"type":"string"}},"readonly":{"description":"List of users with read-only privileges.","type":"array","items":{"type":"string"}}}}}}},"backup":{"description":"DEPRECATED: Backup configuration. Prefer the BackupClass / Plan flow under examples/backups/mariadb/.","type":"object","default":{},"required":["cleanupStrategy","enabled","resticPassword","s3AccessKey","s3Bucket","s3Region","s3SecretKey","schedule"],"properties":{"cleanupStrategy":{"description":"DEPRECATED: Retention strategy for cleaning up old backups.","type":"string","default":"--keep-last=3 --keep-daily=3 --keep-within-weekly=1m"},"enabled":{"description":"DEPRECATED: Enable regular backups (default: false).","type":"boolean","default":false},"resticPassword":{"description":"DEPRECATED: Password for Restic backup encryption.","type":"string","default":"<password>"},"s3AccessKey":{"description":"DEPRECATED: Access key for S3 authentication.","type":"string","default":"<your-access-key>"},"s3Bucket":{"description":"DEPRECATED: S3 bucket used for storing backups.","type":"string","default":"s3.example.org/mariadb-backups"},"s3Region":{"description":Line truncated
{"title":"Chart Values","type":"object","properties":{"replicas":{"description":"Number of MariaDB replicas.","type":"integer","default":2},"resources":{"description":"Explicit CPU and memory configuration for each MariaDB replica. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each replica.","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 (RAM) available to each replica.","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}}},"resourcesPreset":{"description":"Default sizing preset used when `resources` is omitted.","type":"string","default":"t1.nano","enum":["t1.nano","t1.micro","t1.small","t1.medium","t1.large","t1.xlarge","t1.2xlarge","t1.4xlarge","c1.nano","c1.micro","c1.small","c1.medium","c1.large","c1.xlarge","c1.2xlarge","c1.4xlarge","s1.nano","s1.micro","s1.small","s1.medium","s1.large","s1.xlarge","s1.2xlarge","s1.4xlarge","u1.nano","u1.micro","u1.small","u1.medium","u1.large","u1.xlarge","u1.2xlarge","u1.4xlarge","m1.nano","m1.micro","m1.small","m1.medium","m1.large","m1.xlarge","m1.2xlarge","m1.4xlarge","nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume Claim size available for application data.","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},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":"","x-kubernetes-validations":[{"rule":"self == oldSelf","message":"storageClass is immutable"}],"x-cozystack-options":{"source":"storageclass"}},"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"externalIPs":{"description":"Reserved addresses for the endpoints `external: true` publishes. Each entry pins one endpoint to an address held by an IPAddressClaim, so the address survives a rebuild of this application. Empty lets the load balancer assign any free address, as before.","type":"array","default":[],"items":{"type":"object","required":["claim"],"properties":{"claim":{"description":"Name of an IPAddressClaim in this namespace whose address the endpoint should wear. Until the claim binds, the endpoint keeps whatever address the load balancer assigned.","type":"string"},"target":{"description":"Which external endpoint to pin. `primary` is the read-write endpoint, the endpoint `external: true` publishes, and is the default.","type":"string"}}}},"tls":{"description":"TLS configuration. Selects who issues the certificates and whether plaintext is refused; TLS itself is always served.","type":"object","default":{},"properties":{"issuer":{"description":"Who issues the server certificate: `operator` uses the CA mariadb-operator manages itself, `cert-manager` gives the instance its own CA and is what covers the external hostname. Named for the issuer because TLS is served under both, unlike the similarly-spelled `tls.enabled` in some other charts, which does switch TLS on and off.","type":"string","default":"operator","enum":["operator","cert-manager"]},"required":{"description":"Refuse plaintext connections (sets MariaDB require_secure_transport=ON). Applies under either issuer. Opt-in: turn it on once every client connects over TLS, since it disconnects those that do not.","type":"boolean"}}},"version":{"description":"MariaDB major.minor version to deploy","type":"string","default":"v11.8","enum":["v11.8","v11.4","v10.11","v10.6"]},"users":{"description":"Users configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","required":["maxUserConnections","password"],"properties":{"maxUserConnections":{"description":"Maximum number of connections.","type":"integer"},"password":{"description":"Password for the user.","type":"string"}}}},"databases":{"description":"Databases configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","properties":{"roles":{"description":"Roles assigned to users.","type":"object","properties":{"admin":{"description":"List of users with admin privileges.","type":"array","items":{"type":"string"}},"readonly":{"description":"List of users with read-only privileges.","type":"array","items":{"type":"string"}}}}}}},"backup":{"description":"DEPRECATED: Backup configuration. Prefer the BackupClass / Plan flow under examples/backups/mariadb/.","type":"object","default":{},"required":["cleanupStrategy","enabled","resticPassword","s3AccessKey",Line truncated
release:
prefix: mariadb-
labels:
@@ -25,7 +25,7 @@ spec:
tags:
- database
icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9Ii0wLjAwMTk1MzEyIiB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgcng9IjI0IiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfNjgzXzI5MzApIi8+CjxwYXRoIGQ9Ik0xMzMuMTkxIDI5LjAwMjJDMTMxLjIxMyAyOS4wNjU0IDEzMS44MzkgMjkuNjM1NCAxMjcuNTY0IDMwLjY4NzNDMTIzLjI0OCAzMS43NDk2IDExNy45NzUgMzEuNDIzOSAxMTMuMzI3IDMzLjM3MzNDOTkuNDUxNiAzOS4xOTI0IDk2LjY2NzYgNTkuMDgxMyA4NC4wNTM1IDY2LjIwNTlDNzQuNjI0NyA3MS41MzE4IDY1LjExMiA3MS45NTY1IDU2LjU1OTQgNzQuNjM2NUM1MC45Mzg5IDc2LjM5OSA0NC43OTA2IDgwLjAxMzUgMzkuNjk4MiA4NC40MDE4QzM1Ljc0NTUgODcuODA5MyAzNS42NDIzIDkwLjgwNTQgMzEuNTEyMyA5NS4wNzkxQzI3LjA5NDcgOTkuNjUwNCAxMy45NTUxIDk1LjE1NjQgOCAxMDIuMTUzQzkuOTE4MzUgMTA0LjA5MyAxMC43NTk0IDEwNC42MzYgMTQuNTM5OCAxMDQuMTMzQzEzLjc1NzEgMTA1LjYxNiA5LjE0MzMyIDEwNi44NjYgMTAuMDQ2NSAxMDkuMDQ5QzEwLjk5NjggMTExLjM0NSAyMi4xNTExIDExMi45MDEgMzIuMjkwOCAxMDYuNzhDMzcuMDEzMSAxMDMuOTI5IDQwLjc3NDMgOTkuODE5MyA0OC4xMjg4IDk4LjgzODRDNTcuNjQ1OSA5Ny41Njk5IDY4LjYwOTMgOTkuNjUyIDc5LjYyNjggMTAxLjI0MUM3Ny45OTMyIDEwNi4xMTIgNzQuNzEzMyAxMDkuMzUxIDcyLjA4NiAxMTMuMjMxQzcxLjI3MjQgMTE0LjEwNyA3My43MjAyIDExNC4yMDUgNzYuNTEyNiAxMTMuNjc1QzgxLjUzNTkgMTEyLjQzMyA4NS4xNTYxIDExMS40MzMgODguOTQ3MiAxMDkuMjI3QzkzLjYwNDcgMTA2LjUxNSA5NC4zMTA0IDk5LjU2MzkgMTAwLjAyNSA5OC4wNTk5QzEwMy4yMDkgMTAyLjk1NCAxMTEuODY5IDEwNC4xMSAxMTcuMjQyIDEwMC4xOTVDMTEyLjUyNyA5OC44NjA3IDExMS4yMjQgODguODI0NCAxMTIuODE1IDg0LjQwMThDMTE0LjMyMyA4MC4yMTU2IDExNS44MTMgNzMuNTE5MiAxMTcuMzMxIDY3Ljk4NTVDMTE4Ljk2MSA2Mi4wNDI1IDExOS41NjIgNTQuNTUxOSAxMjEuNTM1IDUxLjUyNDdDMTI0LjUwMyA0Ni45NzAxIDEyNy43ODMgNDUuNDA2IDEzMC42MyA0Mi44Mzc3QzEzMy40NzcgNDAuMjY5NSAxMzYuMDgzIDM3Ljc2OTQgMTM1Ljk5OCAzMS44OTI3QzEzNS45NyAyOS45OTk4IDEzNC45OTIgMjguOTQ0NyAxMzMuMTkxIDI5LjAwMjJaIiBmaWxsPSIjMDQyNDRFIi8+CjxwYXRoIGQ9Ik0xMjguOTUzIDMyLjQ4NDRDMTI5LjQyNyAzNC4xMDA0IDEzMC4xNjggMzQuODQyMSAxMzMuMzc1IDM1LjEzODdDMTMyLjkwNiAzOS4yMDQxIDEzMC4xOTUgNDEuNDI3NiAxMjcuMTU0IDQzLjU2MTFDMTI0LjQ3OSA0NS40Mzc2IDEyMS41NDcgNDcuMjQ0NSAxMTkuNjY0IDUwLjE3NTdDMTE3LjczNCA1My4xNzg1IDExNi41MDkgNjMuNDU1NCAxMTMuNTE3IDczLjYwNDRDMTEwLjkzMSA4Mi4zNzM4IDEwNy4wMjUgOTEuMDQ0NSAxMDAuMjA0IDk0Ljg0MzdDOTkuNDkxOSA5My4wNTAyIDEwMC4yOTUgODkuNzQgOTguODc4IDg4LjY1MkM5Ny45NjExIDkxLjI2NzQgOTYuOTI0MiA5My43NjI3IDk1LjcwOTggOTYuMDgyMUM5MS43MDc3IDEwMy43MzIgODUuNzgyMiAxMDkuNDU5IDc1Ljg4MDEgMTExLjIwOEM4MC41Nzg1IDEwNC44NSA4NS4wNzEgOTguMjg0NCA4NS4xNjgzIDg3LjMyNjJDODEuODYxNyA4OC4wNDE3IDgxLjkzMTkgOTUuODUyMiA3OC41MzQ1IDk3Ljk0MDJDNzYuMzU2MyA5OC4xNzcyIDc0LjE0OTggOTguMTc1OCA3MS45MjkxIDk4LjA0MjRDNjIuODA5MSA5Ny40OTU5IDUzLjQ1MzUgOTQuNzU0OSA0NC45MjE5IDk3LjQ5MjNDMzkuMTEyOCA5OS4zNTY4IDM0LjM2MTkgMTAzLjc1NSAyOS40NDI4IDEwNS44ODhDMjMuNjYxNCAxMDguMzk2IDE5LjI4MzEgMTA5LjQyNyAxMi4wODM2IDEwOC4zOTZDMTEuMTY5NSAxMDcuMTY0IDE3LjM1MjYgMTA1LjU3NSAxNi45ODI5IDEwMi45MDJDMTQuMTY1MyAxMDIuNTkgMTIuNTI5MyAxMDMuMjczIDEwLjA4MDEgMTAyLjE2QzEwLjM1MDUgMTAxLjY2MiAxMC43NDc5IDEwMS4yNDcgMTEuMjQ4MyAxMDAuOTAxQzE1LjczNzMgOTcuNzk0IDI4LjQ4ODIgMTAwLjE2NyAzMS45MDA2IDk2LjgxNjdDMzQuMDA3IDk0Ljc1IDM1LjM4ODkgOTIuNTg2NyAzNi44MTk3IDkwLjQ4MzhDMzguMjA3MiA4OC40NDM0IDM5LjY0MTUgODYuNDU5NyA0MS44MjY4IDg0LjY3MTlDNDIuNjMzNyA4NC4wMTE4IDQzLjUxMSA4My4zNTk2IDQ0LjQ0MjEgODIuNzIzQzQ4LjE2NiA4MC4xNzQzIDUyLjc3MjkgNzcuODYyOCA1Ny4zMDY2IDc2LjI2OTRDNjMuNDgyNiA3NC4wOTg0IDY5Ljc0MSA3My45MTk1IDc2LjMyMzcgNzEuNDA0M0M4MC4zOTA0IDY5Ljg1IDg0LjgxMjcgNjcuOTMwMiA4OC40MTc0IDY1LjI0MzlDODkuMjczMyA2NC42MDUxIDkwLjA4MzEgNjMuOTI0NSA5MC44MzE5IDYzLjE5NDlDMTAxLjEyNSA1My4xNjA4IDEwMy4xNjUgMzUuNDYxIDExOS4yMjQgMzMuODExNkMxMjEuMTY2IDMzLjYxMjEgMTIyLjc1NiAzMy42NzY3IDEyNC4yMDMgMzMuNjMyN0MxMjUuODcxIDMzLjU4MyAxMjcuMzQ3IDMzLjM4OTMgMTI4Ljk1MyAzMi40ODQ0Wk0xMDkuMzc1IDg5LjEzMzlDMTA5LjU2NyA5Mi4yMDEzIDExMS4zNDggOTguMjg3MiAxMTIuOTIgOTkuNzY2M0MxMDkuODQxIDEwMC41MTUgMTA0LjUzNyA5OS4yNzggMTAzLjE3NyA5Ny4xMDYyQzEwMy44NzYgOTMuOTcwNyAxMDcuNTE0IDkxLjEwNDEgMTA5LjM3NSA4OS4xMzM5WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTEzMC4xMDkgMzUuOTE4N0MxMjkuNDkgMzcuMjE2OSAxMjguMzA1IDM4Ljg5MDggMTI4LjMwNSA0Mi4xOTU2QzEyOC4zIDQyLjc2MyAxMjcuODc1IDQzLjE1MTcgMTI3Ljg2NyA0Mi4yNzdDMTI3Ljg5OSAzOS4wNDcgMTI4Ljc1NCAzNy42NTA3IDEyOS42NjIgMzUuODE1NUMxMzAuMDg1IDM1LjA2MzYgMTMwLjMzOSAzNS4zNzM4IDEzMC4xMDkgMzUuOTE4N1pNMTI5LjQ4NiAzNS40Mjk3QzEyOC43NTYgMzYuNjY4NCAxMjYuOTk4IDM4LjkyNzggMTI2LjcwNyA0Mi4yMjAzQzEyNi42NTMgNDIuNzg0OCAxMjYuMTk0IDQzLjEzNDMgMTI2LjI2NCA0Mi4yNjE4QzEyNi41ODEgMzkuMDQ3NyAxMjcuOTg2IDM3LjAzNiAxMjkuMDUyIDM1LjI4NzNDMTI5LjUzNiAzNC41NzYxIDEyOS43NjMgMzQuOTA3NCAxMjkuNDg2IDM1LjQyOTdaTTEyOC45MTggMzQuNzgxN0MxMjguMDg2IDM1Ljk1NDMgMTI1LjM4IDM4LjY2NzggMTI0LjgxNCA0MS45MjQ3QzEyNC43MTIgNDIuNDgxOSAxMjQuMjI1IDQyLjc5MjggMTI0LjM2OCA0MS45MjlDMTI0Ljk1NCAzOC43NTIgMTI3LjI4NyAzNi4yNTUgMTI4LjQ5NiAzNC42MDM3QzEyOS4wMzggMzMuOTM0NiAxMjkuMjM3IDM0LjI4NCAxMjguOTE4IDM0Ljc4MTdaTTEyOC40MTEgMzQuMDU4OEwxMjguMTM3IDM0LjM0OTlDMTI2LjkyNyAzNS42NDcyIDEyNC4xMTYgMzguODExNCAxMjMuMTc5IDQxLjcwNzRDMTIyLjk5OSA0Mi4yNDUgMTIyLjQ3NCA0Mi40ODQ4IDEyMi43MzcgNDEuNjQ5M0MxMjMuNzYzIDM4LjU4NjQgMTI2LjU4OSAzNS4yODczIDEyOC4wMTggMzMuODIyN0MxMjguNjUgMzMuMjM2NCAxMjguNzk2IDMzLjYxMDYgMTI4LjQxMSAzNC4wNTg4Wk0xMTMuODg2IDQwLjYxNjJDMTE0LjUxMyAzNy45MjMxIDExNi42MDcgMLine truncated
keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "replicas"], ["spec", "resources"], ["spec", "resourcesPreset"], ["spec", "size"], ["spec", "storageClass"], ["spec", "external"], ["spec", "tls"], ["spec", "tls", "issuer"], ["spec", "version"], ["spec", "users"], ["spec", "databases"], ["spec", "backup"], ["spec", "backup", "enabled"], ["spec", "backup", "s3Region"], ["spec", "backup", "s3Bucket"], ["spec", "backup", "schedule"], ["spec", "backup", "cleanupStrategy"], ["spec", "backup", "s3AccessKey"], ["spec", "backup", "s3SecretKey"], ["spec", "backup", "resticPassword"]]
keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "replicas"], ["spec", "resources"], ["spec", "resourcesPreset"], ["spec", "size"], ["spec", "storageClass"], ["spec", "external"], ["spec", "externalIPs"], ["spec", "tls"], ["spec", "tls", "issuer"], ["spec", "version"], ["spec", "users"], ["spec", "databases"], ["spec", "backup"], ["spec", "backup", "enabled"], ["spec", "backup", "s3Region"], ["spec", "backup", "s3Bucket"], ["spec", "backup", "schedule"], ["spec", "backup", "cleanupStrategy"], ["spec", "backup", "s3AccessKey"], ["spec", "backup", "s3SecretKey"], ["spec", "backup", "resticPassword"]]
# The operator reconciles <release>-ca-bundle whenever TLS is on. It holds
# ca.crt alone, never a private key, and it keeps the previous CA alongside
# the new one across a rollover, so it is the trust anchor to publish from —