From dca7a5fd0dd104bdab6a6291d0ca89286ea8d2a1 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Mon, 7 Sep 2026 18:36:36 +0200 Subject: [PATCH] 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 --- api/apps/v1alpha1/mariadb/types.go | 10 ++ packages/apps/mariadb/README.md | 23 +++-- packages/apps/mariadb/templates/mariadb.yaml | 19 ++++ .../apps/mariadb/tests/externalip_test.yaml | 95 +++++++++++++++++++ packages/apps/mariadb/values.schema.json | 21 ++++ packages/apps/mariadb/values.yaml | 10 ++ .../system/mariadb-rd/cozyrds/mariadb.yaml | 4 +- 7 files changed, 170 insertions(+), 12 deletions(-) create mode 100644 packages/apps/mariadb/tests/externalip_test.yaml diff --git a/api/apps/v1alpha1/mariadb/types.go b/api/apps/v1alpha1/mariadb/types.go index 4a0fa2d79..5f0ff0598 100644 --- a/api/apps/v1alpha1/mariadb/types.go +++ b/api/apps/v1alpha1/mariadb/types.go @@ -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"` diff --git a/packages/apps/mariadb/README.md b/packages/apps/mariadb/README.md index 983d398e1..0ede76fab 100644 --- a/packages/apps/mariadb/README.md +++ b/packages/apps/mariadb/README.md @@ -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 diff --git a/packages/apps/mariadb/templates/mariadb.yaml b/packages/apps/mariadb/templates/mariadb.yaml index 3c219b0b8..f500943c0 100644 --- a/packages/apps/mariadb/templates/mariadb.yaml +++ b/packages/apps/mariadb/templates/mariadb.yaml @@ -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: diff --git a/packages/apps/mariadb/tests/externalip_test.yaml b/packages/apps/mariadb/tests/externalip_test.yaml new file mode 100644 index 000000000..a5a911458 --- /dev/null +++ b/packages/apps/mariadb/tests/externalip_test.yaml @@ -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' diff --git a/packages/apps/mariadb/values.schema.json b/packages/apps/mariadb/values.schema.json index 38edbca1c..6627cca2a 100644 --- a/packages/apps/mariadb/values.schema.json +++ b/packages/apps/mariadb/values.schema.json @@ -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", diff --git a/packages/apps/mariadb/values.yaml b/packages/apps/mariadb/values.yaml index 68199a6af..e53b234c8 100644 --- a/packages/apps/mariadb/values.yaml +++ b/packages/apps/mariadb/values.yaml @@ -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 ## diff --git a/packages/system/mariadb-rd/cozyrds/mariadb.yaml b/packages/system/mariadb-rd/cozyrds/mariadb.yaml index fa743a092..6c77889ff 100644 --- a/packages/system/mariadb-rd/cozyrds/mariadb.yaml +++ b/packages/system/mariadb-rd/cozyrds/mariadb.yaml @@ -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":""},"s3AccessKey":{"description":"DEPRECATED: Access key for S3 authentication.","type":"string","default":""},"s3Bucket":{"description":"DEPRECATED: S3 bucket used for storing backups.","type":"string","default":"s3.example.org/mariadb-backups"},"s3Region":{"description":"DEPRECATED: AWS S3 region where backups are stored.","type":"string","default":"us-east-1"},"s3SecretKey":{"description":"DEPRECATED: Secret key for S3 authentication.","type":"string","default":""},"schedule":{"description":"DEPRECATED: Cron schedule for automated backups.","type":"string","default":"0 2 * * *"}}}}} + {"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","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":""},"s3AccessKey":{"description":"DEPRECATED: Access key for S3 authentication.","type":"string","default":""},"s3Bucket":{"description":"DEPRECATED: S3 bucket used for storing backups.","type":"string","default":"s3.example.org/mariadb-backups"},"s3Region":{"description":"DEPRECATED: AWS S3 region where backups are stored.","type":"string","default":"us-east-1"},"s3SecretKey":{"description":"DEPRECATED: Secret key for S3 authentication.","type":"string","default":""},"schedule":{"description":"DEPRECATED: Cron schedule for automated backups.","type":"string","default":"0 2 * * *"}}}}} release: prefix: mariadb- labels: @@ -25,7 +25,7 @@ spec: tags: - database icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9Ii0wLjAwMTk1MzEyIiB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgcng9IjI0IiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfNjgzXzI5MzApIi8+CjxwYXRoIGQ9Ik0xMzMuMTkxIDI5LjAwMjJDMTMxLjIxMyAyOS4wNjU0IDEzMS44MzkgMjkuNjM1NCAxMjcuNTY0IDMwLjY4NzNDMTIzLjI0OCAzMS43NDk2IDExNy45NzUgMzEuNDIzOSAxMTMuMzI3IDMzLjM3MzNDOTkuNDUxNiAzOS4xOTI0IDk2LjY2NzYgNTkuMDgxMyA4NC4wNTM1IDY2LjIwNTlDNzQuNjI0NyA3MS41MzE4IDY1LjExMiA3MS45NTY1IDU2LjU1OTQgNzQuNjM2NUM1MC45Mzg5IDc2LjM5OSA0NC43OTA2IDgwLjAxMzUgMzkuNjk4MiA4NC40MDE4QzM1Ljc0NTUgODcuODA5MyAzNS42NDIzIDkwLjgwNTQgMzEuNTEyMyA5NS4wNzkxQzI3LjA5NDcgOTkuNjUwNCAxMy45NTUxIDk1LjE1NjQgOCAxMDIuMTUzQzkuOTE4MzUgMTA0LjA5MyAxMC43NTk0IDEwNC42MzYgMTQuNTM5OCAxMDQuMTMzQzEzLjc1NzEgMTA1LjYxNiA5LjE0MzMyIDEwNi44NjYgMTAuMDQ2NSAxMDkuMDQ5QzEwLjk5NjggMTExLjM0NSAyMi4xNTExIDExMi45MDEgMzIuMjkwOCAxMDYuNzhDMzcuMDEzMSAxMDMuOTI5IDQwLjc3NDMgOTkuODE5MyA0OC4xMjg4IDk4LjgzODRDNTcuNjQ1OSA5Ny41Njk5IDY4LjYwOTMgOTkuNjUyIDc5LjYyNjggMTAxLjI0MUM3Ny45OTMyIDEwNi4xMTIgNzQuNzEzMyAxMDkuMzUxIDcyLjA4NiAxMTMuMjMxQzcxLjI3MjQgMTE0LjEwNyA3My43MjAyIDExNC4yMDUgNzYuNTEyNiAxMTMuNjc1QzgxLjUzNTkgMTEyLjQzMyA4NS4xNTYxIDExMS40MzMgODguOTQ3MiAxMDkuMjI3QzkzLjYwNDcgMTA2LjUxNSA5NC4zMTA0IDk5LjU2MzkgMTAwLjAyNSA5OC4wNTk5QzEwMy4yMDkgMTAyLjk1NCAxMTEuODY5IDEwNC4xMSAxMTcuMjQyIDEwMC4xOTVDMTEyLjUyNyA5OC44NjA3IDExMS4yMjQgODguODI0NCAxMTIuODE1IDg0LjQwMThDMTE0LjMyMyA4MC4yMTU2IDExNS44MTMgNzMuNTE5MiAxMTcuMzMxIDY3Ljk4NTVDMTE4Ljk2MSA2Mi4wNDI1IDExOS41NjIgNTQuNTUxOSAxMjEuNTM1IDUxLjUyNDdDMTI0LjUwMyA0Ni45NzAxIDEyNy43ODMgNDUuNDA2IDEzMC42MyA0Mi44Mzc3QzEzMy40NzcgNDAuMjY5NSAxMzYuMDgzIDM3Ljc2OTQgMTM1Ljk5OCAzMS44OTI3QzEzNS45NyAyOS45OTk4IDEzNC45OTIgMjguOTQ0NyAxMzMuMTkxIDI5LjAwMjJaIiBmaWxsPSIjMDQyNDRFIi8+CjxwYXRoIGQ9Ik0xMjguOTUzIDMyLjQ4NDRDMTI5LjQyNyAzNC4xMDA0IDEzMC4xNjggMzQuODQyMSAxMzMuMzc1IDM1LjEzODdDMTMyLjkwNiAzOS4yMDQxIDEzMC4xOTUgNDEuNDI3NiAxMjcuMTU0IDQzLjU2MTFDMTI0LjQ3OSA0NS40Mzc2IDEyMS41NDcgNDcuMjQ0NSAxMTkuNjY0IDUwLjE3NTdDMTE3LjczNCA1My4xNzg1IDExNi41MDkgNjMuNDU1NCAxMTMuNTE3IDczLjYwNDRDMTEwLjkzMSA4Mi4zNzM4IDEwNy4wMjUgOTEuMDQ0NSAxMDAuMjA0IDk0Ljg0MzdDOTkuNDkxOSA5My4wNTAyIDEwMC4yOTUgODkuNzQgOTguODc4IDg4LjY1MkM5Ny45NjExIDkxLjI2NzQgOTYuOTI0MiA5My43NjI3IDk1LjcwOTggOTYuMDgyMUM5MS43MDc3IDEwMy43MzIgODUuNzgyMiAxMDkuNDU5IDc1Ljg4MDEgMTExLjIwOEM4MC41Nzg1IDEwNC44NSA4NS4wNzEgOTguMjg0NCA4NS4xNjgzIDg3LjMyNjJDODEuODYxNyA4OC4wNDE3IDgxLjkzMTkgOTUuODUyMiA3OC41MzQ1IDk3Ljk0MDJDNzYuMzU2MyA5OC4xNzcyIDc0LjE0OTggOTguMTc1OCA3MS45MjkxIDk4LjA0MjRDNjIuODA5MSA5Ny40OTU5IDUzLjQ1MzUgOTQuNzU0OSA0NC45MjE5IDk3LjQ5MjNDMzkuMTEyOCA5OS4zNTY4IDM0LjM2MTkgMTAzLjc1NSAyOS40NDI4IDEwNS44ODhDMjMuNjYxNCAxMDguMzk2IDE5LjI4MzEgMTA5LjQyNyAxMi4wODM2IDEwOC4zOTZDMTEuMTY5NSAxMDcuMTY0IDE3LjM1MjYgMTA1LjU3NSAxNi45ODI5IDEwMi45MDJDMTQuMTY1MyAxMDIuNTkgMTIuNTI5MyAxMDMuMjczIDEwLjA4MDEgMTAyLjE2QzEwLjM1MDUgMTAxLjY2MiAxMC43NDc5IDEwMS4yNDcgMTEuMjQ4MyAxMDAuOTAxQzE1LjczNzMgOTcuNzk0IDI4LjQ4ODIgMTAwLjE2NyAzMS45MDA2IDk2LjgxNjdDMzQuMDA3IDk0Ljc1IDM1LjM4ODkgOTIuNTg2NyAzNi44MTk3IDkwLjQ4MzhDMzguMjA3MiA4OC40NDM0IDM5LjY0MTUgODYuNDU5NyA0MS44MjY4IDg0LjY3MTlDNDIuNjMzNyA4NC4wMTE4IDQzLjUxMSA4My4zNTk2IDQ0LjQ0MjEgODIuNzIzQzQ4LjE2NiA4MC4xNzQzIDUyLjc3MjkgNzcuODYyOCA1Ny4zMDY2IDc2LjI2OTRDNjMuNDgyNiA3NC4wOTg0IDY5Ljc0MSA3My45MTk1IDc2LjMyMzcgNzEuNDA0M0M4MC4zOTA0IDY5Ljg1IDg0LjgxMjcgNjcuOTMwMiA4OC40MTc0IDY1LjI0MzlDODkuMjczMyA2NC42MDUxIDkwLjA4MzEgNjMuOTI0NSA5MC44MzE5IDYzLjE5NDlDMTAxLjEyNSA1My4xNjA4IDEwMy4xNjUgMzUuNDYxIDExOS4yMjQgMzMuODExNkMxMjEuMTY2IDMzLjYxMjEgMTIyLjc1NiAzMy42NzY3IDEyNC4yMDMgMzMuNjMyN0MxMjUuODcxIDMzLjU4MyAxMjcuMzQ3IDMzLjM4OTMgMTI4Ljk1MyAzMi40ODQ0Wk0xMDkuMzc1IDg5LjEzMzlDMTA5LjU2NyA5Mi4yMDEzIDExMS4zNDggOTguMjg3MiAxMTIuOTIgOTkuNzY2M0MxMDkuODQxIDEwMC41MTUgMTA0LjUzNyA5OS4yNzggMTAzLjE3NyA5Ny4xMDYyQzEwMy44NzYgOTMuOTcwNyAxMDcuNTE0IDkxLjEwNDEgMTA5LjM3NSA4OS4xMzM5WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTEzMC4xMDkgMzUuOTE4N0MxMjkuNDkgMzcuMjE2OSAxMjguMzA1IDM4Ljg5MDggMTI4LjMwNSA0Mi4xOTU2QzEyOC4zIDQyLjc2MyAxMjcuODc1IDQzLjE1MTcgMTI3Ljg2NyA0Mi4yNzdDMTI3Ljg5OSAzOS4wNDcgMTI4Ljc1NCAzNy42NTA3IDEyOS42NjIgMzUuODE1NUMxMzAuMDg1IDM1LjA2MzYgMTMwLjMzOSAzNS4zNzM4IDEzMC4xMDkgMzUuOTE4N1pNMTI5LjQ4NiAzNS40Mjk3QzEyOC43NTYgMzYuNjY4NCAxMjYuOTk4IDM4LjkyNzggMTI2LjcwNyA0Mi4yMjAzQzEyNi42NTMgNDIuNzg0OCAxMjYuMTk0IDQzLjEzNDMgMTI2LjI2NCA0Mi4yNjE4QzEyNi41ODEgMzkuMDQ3NyAxMjcuOTg2IDM3LjAzNiAxMjkuMDUyIDM1LjI4NzNDMTI5LjUzNiAzNC41NzYxIDEyOS43NjMgMzQuOTA3NCAxMjkuNDg2IDM1LjQyOTdaTTEyOC45MTggMzQuNzgxN0MxMjguMDg2IDM1Ljk1NDMgMTI1LjM4IDM4LjY2NzggMTI0LjgxNCA0MS45MjQ3QzEyNC43MTIgNDIuNDgxOSAxMjQuMjI1IDQyLjc5MjggMTI0LjM2OCA0MS45MjlDMTI0Ljk1NCAzOC43NTIgMTI3LjI4NyAzNi4yNTUgMTI4LjQ5NiAzNC42MDM3QzEyOS4wMzggMzMuOTM0NiAxMjkuMjM3IDM0LjI4NCAxMjguOTE4IDM0Ljc4MTdaTTEyOC40MTEgMzQuMDU4OEwxMjguMTM3IDM0LjM0OTlDMTI2LjkyNyAzNS42NDcyIDEyNC4xMTYgMzguODExNCAxMjMuMTc5IDQxLjcwNzRDMTIyLjk5OSA0Mi4yNDUgMTIyLjQ3NCA0Mi40ODQ4IDEyMi43MzcgNDEuNjQ5M0MxMjMuNzYzIDM4LjU4NjQgMTI2LjU4OSAzNS4yODczIDEyOC4wMTggMzMuODIyN0MxMjguNjUgMzMuMjM2NCAxMjguNzk2IDMzLjYxMDYgMTI4LjQxMSAzNC4wNTg4Wk0xMTMuODg2IDQwLjYxNjJDMTE0LjUxMyAzNy45MjMxIDExNi42MDcgMzYuNjk2IDEyMC4yMjMgMzYuOTk1M0MxMjEuMDk2IDQxLjAxNTEgMTE2LjIxMyA0Mi42MzY2IDExMy44ODYgNDAuNjE2MloiIGZpbGw9IiMwNDI0NEUiLz4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhcl82ODNfMjkzMCIgeDE9IjE0MC41IiB5MT0iMTQxIiB4Mj0iNS45OTk5OSIgeTI9Ii01LjUwMjI4ZS0wNiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjQzQ5QTZDIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0U3QkY5MyIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo= - 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 -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 —