diff --git a/hack/e2e-apps/etcd.bats b/hack/e2e-apps/etcd.bats new file mode 100644 index 00000000..abc290db --- /dev/null +++ b/hack/e2e-apps/etcd.bats @@ -0,0 +1,113 @@ +#!/usr/bin/env bats + +# The etcd chart pins the Helm release name to 'etcd' via +# packages/extra/etcd/templates/check-release-name.yaml, so every test +# must apply its Etcd with metadata.name: etcd. setup() clears any +# prior run so tests remain independent despite the singleton name. + +setup() { + kubectl -n tenant-test delete etcd.apps.cozystack.io --all --ignore-not-found --timeout=2m + # HelmRelease teardown is async relative to the CR deletion above; wait for + # downstream resources so the next test starts from a clean state. + kubectl -n tenant-test wait hr/etcd --for=delete --timeout=2m --ignore-not-found + kubectl -n tenant-test wait secret/etcd-s3-creds --for=delete --timeout=1m --ignore-not-found + kubectl -n tenant-test wait etcdbackupschedule.etcd.aenix.io/etcd --for=delete --timeout=1m --ignore-not-found +} + +dump_diagnostics() { + echo "# --- diagnostics ---" >&3 + kubectl -n tenant-test get etcdcluster,etcdbackupschedule,cronjob -o wide >&3 2>&1 || true + kubectl -n tenant-test describe etcdbackupschedule etcd >&3 2>&1 || true + kubectl -n cozy-etcd-operator logs -l app.kubernetes.io/name=etcd-operator --tail=100 >&3 2>&1 || true +} + +# Wait until the etcd HelmRelease is reconciled by Flux and its +# condition=ready is set. kubectl wait fails immediately on a missing +# resource, so poll for existence first. +wait_etcd_hr_ready() { + timeout 60 sh -ec 'until kubectl -n tenant-test get hr/etcd >/dev/null 2>&1; do sleep 2; done' + kubectl -n tenant-test wait hr/etcd --timeout=60s --for=condition=ready +} + +@test "Create Etcd" { + kubectl apply -f- </dev/null + ! kubectl -n tenant-test get secret etcd-s3-creds 2>/dev/null +} + +@test "Create Etcd with backup schedule" { + kubectl apply -f- </dev/null)\" != '' ]; do sleep 5; done" || { dump_diagnostics; false; } +} diff --git a/packages/core/platform/sources/etcd-operator.yaml b/packages/core/platform/sources/etcd-operator.yaml index 5f42a4a0..a262f94e 100644 --- a/packages/core/platform/sources/etcd-operator.yaml +++ b/packages/core/platform/sources/etcd-operator.yaml @@ -21,3 +21,4 @@ spec: install: namespace: cozy-etcd-operator releaseName: etcd-operator + upgradeCRDs: CreateReplace diff --git a/packages/extra/etcd/Makefile b/packages/extra/etcd/Makefile index f37d6e1e..2b3ed61e 100644 --- a/packages/extra/etcd/Makefile +++ b/packages/extra/etcd/Makefile @@ -5,3 +5,6 @@ include ../../../hack/package.mk generate: cozyvalues-gen -v values.yaml -s values.schema.json -r README.md ../../../hack/update-crd.sh + +test: + helm unittest . diff --git a/packages/extra/etcd/README.md b/packages/extra/etcd/README.md index daf9f114..fb146c59 100644 --- a/packages/extra/etcd/README.md +++ b/packages/extra/etcd/README.md @@ -1,5 +1,13 @@ # Etcd-cluster +## Backups + +When `backup.enabled` is set to `true`, the chart renders an `EtcdBackupSchedule` (etcd.aenix.io/v1alpha1) and an S3 credentials `Secret`. The etcd-operator v0.4.3+ reconciles the schedule into a `CronJob` that periodically snapshots the cluster to S3. + +Enabling backup requires the following fields to be explicitly set (defaults are empty strings so that missing values fail fast at template render time): `backup.s3AccessKey`, `backup.s3SecretKey`, `backup.destinationPath` (must start with `s3://` and have no `//` segments), and `backup.endpointURL`. S3 credentials passed through plain values end up in the HelmRelease manifest — for production deployments prefer an external secret management tool (ESO, Sealed Secrets, etc.) over committing the keys to Git. + +**Restore** (`EtcdCluster.spec.bootstrap`) and the one-shot `EtcdBackup` custom resource shipped upstream in v0.4.3 are not yet exposed through this chart. Restoring from a snapshot or taking an ad-hoc backup currently requires hand-applying the corresponding custom resource manifest. + ## Parameters ### Common parameters @@ -13,3 +21,20 @@ | `resources.cpu` | Number of CPU cores allocated. | `quantity` | `1000m` | | `resources.memory` | Amount of memory allocated. | `quantity` | `512Mi` | + +### Backup parameters + +| Name | Description | Type | Value | +| ----------------------------------- | ----------------------------------------------------------------------------- | -------- | ----------- | +| `backup` | Backup configuration. | `object` | `{}` | +| `backup.enabled` | Enable scheduled S3 backups. | `bool` | `false` | +| `backup.schedule` | Cron schedule for automated backups. | `string` | `0 2 * * *` | +| `backup.destinationPath` | Destination path for backups (e.g. s3://bucket/path/). | `string` | `""` | +| `backup.endpointURL` | S3 endpoint URL for uploads. | `string` | `""` | +| `backup.region` | S3 region. | `string` | `""` | +| `backup.forcePathStyle` | Use path-style S3 URLs (required for MinIO and most S3-compatible providers). | `bool` | `true` | +| `backup.s3AccessKey` | Access key for S3 authentication. | `string` | `""` | +| `backup.s3SecretKey` | Secret key for S3 authentication. | `string` | `""` | +| `backup.successfulJobsHistoryLimit` | Number of successful backup jobs to retain. | `int` | `3` | +| `backup.failedJobsHistoryLimit` | Number of failed backup jobs to retain. | `int` | `1` | + diff --git a/packages/extra/etcd/templates/backup-secret.yaml b/packages/extra/etcd/templates/backup-secret.yaml new file mode 100644 index 00000000..080bff21 --- /dev/null +++ b/packages/extra/etcd/templates/backup-secret.yaml @@ -0,0 +1,11 @@ +{{- if .Values.backup.enabled }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: etcd-s3-creds +type: Opaque +stringData: + AWS_ACCESS_KEY_ID: {{ required "backup.s3AccessKey is required when backup.enabled=true" .Values.backup.s3AccessKey | quote }} + AWS_SECRET_ACCESS_KEY: {{ required "backup.s3SecretKey is required when backup.enabled=true" .Values.backup.s3SecretKey | quote }} +{{- end }} diff --git a/packages/extra/etcd/templates/etcd-backup-schedule.yaml b/packages/extra/etcd/templates/etcd-backup-schedule.yaml new file mode 100644 index 00000000..3dce5a9e --- /dev/null +++ b/packages/extra/etcd/templates/etcd-backup-schedule.yaml @@ -0,0 +1,35 @@ +{{- if .Values.backup.enabled }} +{{- $endpointURL := required "backup.endpointURL is required when backup.enabled=true" .Values.backup.endpointURL -}} +{{- $destPath := required "backup.destinationPath is required when backup.enabled=true" .Values.backup.destinationPath -}} +{{- if not (hasPrefix "s3://" $destPath) }}{{ fail "backup.destinationPath must start with s3://" }}{{- end }} +{{- $rawPath := $destPath | trimPrefix "s3://" | trimSuffix "/" -}} +{{- if contains "//" $rawPath }}{{ fail "backup.destinationPath must not contain '//' segments" }}{{- end }} +{{- $parts := splitList "/" $rawPath -}} +{{- $bucket := first $parts -}} +{{- if eq $bucket "" }}{{ fail "backup.destinationPath must contain a bucket (e.g. s3://bucket/path/)" }}{{- end }} +{{- $key := rest $parts | join "/" -}} +--- +apiVersion: etcd.aenix.io/v1alpha1 +kind: EtcdBackupSchedule +metadata: + name: etcd +spec: + clusterRef: + name: etcd + schedule: {{ .Values.backup.schedule | quote }} + successfulJobsHistoryLimit: {{ .Values.backup.successfulJobsHistoryLimit | int }} + failedJobsHistoryLimit: {{ .Values.backup.failedJobsHistoryLimit | int }} + destination: + s3: + endpoint: {{ $endpointURL | quote }} + bucket: {{ $bucket | quote }} + {{- with $key }} + key: {{ . | quote }} + {{- end }} + {{- with .Values.backup.region }} + region: {{ . | quote }} + {{- end }} + forcePathStyle: {{ .Values.backup.forcePathStyle }} + credentialsSecretRef: + name: etcd-s3-creds +{{- end }} diff --git a/packages/extra/etcd/tests/backup-secret_test.yaml b/packages/extra/etcd/tests/backup-secret_test.yaml new file mode 100644 index 00000000..02d81977 --- /dev/null +++ b/packages/extra/etcd/tests/backup-secret_test.yaml @@ -0,0 +1,104 @@ +suite: backup secret tests + +templates: + - templates/backup-secret.yaml + +tests: + - it: does not render when backup disabled + release: + name: etcd + namespace: tenant-test + set: + backup: + enabled: false + asserts: + - hasDocuments: + count: 0 + + - it: renders when backup enabled with valid credentials + release: + name: etcd + namespace: tenant-test + set: + backup: + enabled: true + s3AccessKey: "AKIAIOSFODNN7EXAMPLE" + s3SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + destinationPath: "s3://bucket/path/" + endpointURL: "https://s3.example.com" + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Secret + - equal: + path: metadata.name + value: etcd-s3-creds + - equal: + path: type + value: Opaque + + - it: secret name is hardcoded regardless of release name + release: + name: etcd + namespace: tenant-test + set: + backup: + enabled: true + s3AccessKey: "k" + s3SecretKey: "s" + destinationPath: "s3://b/" + endpointURL: "https://s3.example.com" + asserts: + - equal: + path: metadata.name + value: etcd-s3-creds + + - it: contains AWS credentials in expected keys + release: + name: etcd + namespace: tenant-test + set: + backup: + enabled: true + s3AccessKey: "MYACCESSKEY" + s3SecretKey: "MYSECRETKEY" + destinationPath: "s3://b/" + endpointURL: "https://s3.example.com" + asserts: + - equal: + path: stringData.AWS_ACCESS_KEY_ID + value: "MYACCESSKEY" + - equal: + path: stringData.AWS_SECRET_ACCESS_KEY + value: "MYSECRETKEY" + + - it: fails when s3AccessKey is empty + release: + name: etcd + namespace: tenant-test + set: + backup: + enabled: true + s3AccessKey: "" + s3SecretKey: "secretkey" + destinationPath: "s3://b/" + endpointURL: "https://s3.example.com" + asserts: + - failedTemplate: + errorMessage: "backup.s3AccessKey is required when backup.enabled=true" + + - it: fails when s3SecretKey is empty + release: + name: etcd + namespace: tenant-test + set: + backup: + enabled: true + s3AccessKey: "accesskey" + s3SecretKey: "" + destinationPath: "s3://b/" + endpointURL: "https://s3.example.com" + asserts: + - failedTemplate: + errorMessage: "backup.s3SecretKey is required when backup.enabled=true" diff --git a/packages/extra/etcd/tests/etcd-backup-schedule_test.yaml b/packages/extra/etcd/tests/etcd-backup-schedule_test.yaml new file mode 100644 index 00000000..0f019712 --- /dev/null +++ b/packages/extra/etcd/tests/etcd-backup-schedule_test.yaml @@ -0,0 +1,174 @@ +suite: etcd backup schedule tests + +templates: + - templates/etcd-backup-schedule.yaml + +tests: + - it: does not render when backup disabled + release: + name: etcd + namespace: tenant-test + set: + backup: + enabled: false + asserts: + - hasDocuments: + count: 0 + + - it: renders when backup enabled with valid values + release: + name: etcd + namespace: tenant-test + set: + backup: + enabled: true + schedule: "0 2 * * *" + destinationPath: "s3://my-bucket/etcd/prod/" + endpointURL: "https://s3.example.com" + region: "us-east-1" + forcePathStyle: true + s3AccessKey: "k" + s3SecretKey: "s" + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 1 + asserts: + - hasDocuments: + count: 1 + - isKind: + of: EtcdBackupSchedule + - equal: + path: metadata.name + value: etcd + - equal: + path: spec.clusterRef.name + value: etcd + - equal: + path: spec.schedule + value: "0 2 * * *" + - equal: + path: spec.destination.s3.endpoint + value: "https://s3.example.com" + - equal: + path: spec.destination.s3.bucket + value: "my-bucket" + - equal: + path: spec.destination.s3.key + value: "etcd/prod" + - equal: + path: spec.destination.s3.region + value: "us-east-1" + - equal: + path: spec.destination.s3.forcePathStyle + value: true + - equal: + path: spec.destination.s3.credentialsSecretRef.name + value: etcd-s3-creds + + - it: omits key when destinationPath points at bucket root + release: + name: etcd + namespace: tenant-test + set: + backup: + enabled: true + destinationPath: "s3://bucket-only/" + endpointURL: "https://s3.example.com" + s3AccessKey: "k" + s3SecretKey: "s" + asserts: + - equal: + path: spec.destination.s3.bucket + value: "bucket-only" + - notExists: + path: spec.destination.s3.key + + - it: omits region when not set + release: + name: etcd + namespace: tenant-test + set: + backup: + enabled: true + destinationPath: "s3://b/k/" + endpointURL: "https://s3.example.com" + region: "" + s3AccessKey: "k" + s3SecretKey: "s" + asserts: + - notExists: + path: spec.destination.s3.region + + - it: fails when destinationPath is empty + release: + name: etcd + namespace: tenant-test + set: + backup: + enabled: true + destinationPath: "" + endpointURL: "https://s3.example.com" + s3AccessKey: "k" + s3SecretKey: "s" + asserts: + - failedTemplate: + errorMessage: "backup.destinationPath is required when backup.enabled=true" + + - it: fails when endpointURL is empty + release: + name: etcd + namespace: tenant-test + set: + backup: + enabled: true + destinationPath: "s3://b/" + endpointURL: "" + s3AccessKey: "k" + s3SecretKey: "s" + asserts: + - failedTemplate: + errorMessage: "backup.endpointURL is required when backup.enabled=true" + + - it: fails when destinationPath lacks s3:// prefix + release: + name: etcd + namespace: tenant-test + set: + backup: + enabled: true + destinationPath: "my-bucket/path/" + endpointURL: "https://s3.example.com" + s3AccessKey: "k" + s3SecretKey: "s" + asserts: + - failedTemplate: + errorMessage: "backup.destinationPath must start with s3://" + + - it: fails when destinationPath contains double slashes + release: + name: etcd + namespace: tenant-test + set: + backup: + enabled: true + destinationPath: "s3://bucket//subdir/" + endpointURL: "https://s3.example.com" + s3AccessKey: "k" + s3SecretKey: "s" + asserts: + - failedTemplate: + errorMessage: "backup.destinationPath must not contain '//' segments" + + - it: fails when destinationPath has no bucket + release: + name: etcd + namespace: tenant-test + set: + backup: + enabled: true + destinationPath: "s3:///path/" + endpointURL: "https://s3.example.com" + s3AccessKey: "k" + s3SecretKey: "s" + asserts: + - failedTemplate: + errorMessage: "backup.destinationPath must contain a bucket (e.g. s3://bucket/path/)" diff --git a/packages/extra/etcd/values.schema.json b/packages/extra/etcd/values.schema.json index 3c66664c..cbe259d1 100644 --- a/packages/extra/etcd/values.schema.json +++ b/packages/extra/etcd/values.schema.json @@ -60,6 +60,63 @@ "x-kubernetes-int-or-string": true } } + }, + "backup": { + "description": "Backup configuration.", + "type": "object", + "default": {}, + "properties": { + "destinationPath": { + "description": "Destination path for backups (e.g. s3://bucket/path/).", + "type": "string", + "default": "" + }, + "enabled": { + "description": "Enable scheduled S3 backups.", + "type": "boolean", + "default": false + }, + "endpointURL": { + "description": "S3 endpoint URL for uploads.", + "type": "string", + "default": "" + }, + "failedJobsHistoryLimit": { + "description": "Number of failed backup jobs to retain.", + "type": "integer", + "default": 1 + }, + "forcePathStyle": { + "description": "Use path-style S3 URLs (required for MinIO and most S3-compatible providers).", + "type": "boolean", + "default": true + }, + "region": { + "description": "S3 region.", + "type": "string", + "default": "" + }, + "s3AccessKey": { + "description": "Access key for S3 authentication.", + "type": "string", + "default": "" + }, + "s3SecretKey": { + "description": "Secret key for S3 authentication.", + "type": "string", + "default": "" + }, + "schedule": { + "description": "Cron schedule for automated backups.", + "type": "string", + "default": "0 2 * * *" + }, + "successfulJobsHistoryLimit": { + "description": "Number of successful backup jobs to retain.", + "type": "integer", + "default": 3 + } + } } } } diff --git a/packages/extra/etcd/values.yaml b/packages/extra/etcd/values.yaml index 8dac251d..897dfe48 100644 --- a/packages/extra/etcd/values.yaml +++ b/packages/extra/etcd/values.yaml @@ -19,3 +19,32 @@ replicas: 3 resources: cpu: 1000m memory: 512Mi + +## +## @section Backup parameters +## + +## @typedef {struct} Backup - Backup configuration. +## @field {bool} [enabled] - Enable scheduled S3 backups. +## @field {string} [schedule] - Cron schedule for automated backups. +## @field {string} [destinationPath] - Destination path for backups (e.g. s3://bucket/path/). +## @field {string} [endpointURL] - S3 endpoint URL for uploads. +## @field {string} [region] - S3 region. +## @field {bool} [forcePathStyle] - Use path-style S3 URLs (required for MinIO and most S3-compatible providers). +## @field {string} [s3AccessKey] - Access key for S3 authentication. +## @field {string} [s3SecretKey] - Secret key for S3 authentication. +## @field {int} [successfulJobsHistoryLimit] - Number of successful backup jobs to retain. +## @field {int} [failedJobsHistoryLimit] - Number of failed backup jobs to retain. + +## @param {Backup} backup - Backup configuration. +backup: + enabled: false + schedule: "0 2 * * *" + destinationPath: "" + endpointURL: "" + region: "" + forcePathStyle: true + s3AccessKey: "" + s3SecretKey: "" + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 1 diff --git a/packages/system/etcd-operator/charts/etcd-operator/Chart.yaml b/packages/system/etcd-operator/charts/etcd-operator/Chart.yaml index 9ab731ce..44504579 100644 --- a/packages/system/etcd-operator/charts/etcd-operator/Chart.yaml +++ b/packages/system/etcd-operator/charts/etcd-operator/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v2 -appVersion: v0.4.2 +appVersion: v0.4.3 name: etcd-operator type: application -version: 0.4.2 +version: 0.4.3 diff --git a/packages/system/etcd-operator/charts/etcd-operator/crds/etcd-backup-schedule.yaml b/packages/system/etcd-operator/charts/etcd-operator/crds/etcd-backup-schedule.yaml new file mode 100644 index 00000000..465aa992 --- /dev/null +++ b/packages/system/etcd-operator/charts/etcd-operator/crds/etcd-backup-schedule.yaml @@ -0,0 +1,227 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: etcd-operator-system/etcd-operator-serving-cert + controller-gen.kubebuilder.io/version: v0.15.0 + name: etcdbackupschedules.etcd.aenix.io +spec: + conversion: + strategy: None + group: etcd.aenix.io + names: + kind: EtcdBackupSchedule + listKind: EtcdBackupScheduleList + plural: etcdbackupschedules + singular: etcdbackupschedule + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.clusterRef.name + name: Cluster + type: string + - jsonPath: .spec.schedule + name: Schedule + type: string + - jsonPath: .status.lastSuccessfulBackupTime + name: Last Backup + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: EtcdBackupSchedule is the Schema for the etcdbackupschedules API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: EtcdBackupScheduleSpec defines the desired state of EtcdBackupSchedule + properties: + clusterRef: + description: ClusterRef references the EtcdCluster to back up. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + type: object + x-kubernetes-map-type: atomic + destination: + description: Destination defines where the backup will be stored. + properties: + pvc: + description: PVC defines a PersistentVolumeClaim as the backup destination. + properties: + claimName: + description: ClaimName is the name of the PersistentVolumeClaim to use. + type: string + subPath: + description: |- + SubPath is an optional sub-directory within the PVC volume. + The operator appends the backup filename automatically. + type: string + required: + - claimName + type: object + s3: + description: S3 defines S3-compatible storage as the backup destination. + properties: + bucket: + description: Bucket is the name of the S3 bucket. + type: string + credentialsSecretRef: + description: CredentialsSecretRef references a Secret containing AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY keys. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + type: object + x-kubernetes-map-type: atomic + endpoint: + description: Endpoint is the S3-compatible endpoint URL (e.g., "https://s3.amazonaws.com"). + type: string + forcePathStyle: + description: |- + ForcePathStyle forces path-style S3 URLs (e.g., endpoint/bucket/key) + instead of virtual-hosted-style (e.g., bucket.endpoint/key). + Most S3-compatible providers (MinIO, Ceph) require path style. + type: boolean + key: + description: |- + Key is the key prefix (directory path) within the bucket. + The operator appends the backup filename automatically. + type: string + region: + description: Region is the AWS region for the S3 bucket. + type: string + required: + - bucket + - credentialsSecretRef + - endpoint + type: object + type: object + failedJobsHistoryLimit: + description: FailedJobsHistoryLimit is the number of failed finished CronJob children to retain. + format: int32 + minimum: 0 + type: integer + schedule: + description: Schedule is a cron expression defining when backups should be taken. + type: string + successfulJobsHistoryLimit: + description: SuccessfulJobsHistoryLimit is the number of successful finished CronJob children to retain. + format: int32 + minimum: 0 + type: integer + required: + - clusterRef + - destination + - schedule + type: object + status: + description: EtcdBackupScheduleStatus defines the observed state of EtcdBackupSchedule + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current state of this API Resource.\n---\nThis struct is intended for direct use as an array at the field path .status.conditions. For example,\n\n\n\ttype FooStatus struct{\n\t // Represents the observations of a foo's current state.\n\t // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastScheduleTime: + format: date-time + type: string + lastSuccessfulBackupTime: + format: date-time + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/packages/system/etcd-operator/charts/etcd-operator/crds/etcd-backup.yaml b/packages/system/etcd-operator/charts/etcd-operator/crds/etcd-backup.yaml new file mode 100644 index 00000000..c94c56ea --- /dev/null +++ b/packages/system/etcd-operator/charts/etcd-operator/crds/etcd-backup.yaml @@ -0,0 +1,206 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: etcd-operator-system/etcd-operator-serving-cert + controller-gen.kubebuilder.io/version: v0.15.0 + name: etcdbackups.etcd.aenix.io +spec: + conversion: + strategy: None + group: etcd.aenix.io + names: + kind: EtcdBackup + listKind: EtcdBackupList + plural: etcdbackups + singular: etcdbackup + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.clusterRef.name + name: Cluster + type: string + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: EtcdBackup is the Schema for the etcdbackups API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: EtcdBackupSpec defines the desired state of EtcdBackup + properties: + clusterRef: + description: ClusterRef references the EtcdCluster to back up. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + type: object + x-kubernetes-map-type: atomic + destination: + description: Destination defines where the backup will be stored. + properties: + pvc: + description: PVC defines a PersistentVolumeClaim as the backup destination. + properties: + claimName: + description: ClaimName is the name of the PersistentVolumeClaim to use. + type: string + subPath: + description: |- + SubPath is an optional sub-directory within the PVC volume. + The operator appends the backup filename automatically. + type: string + required: + - claimName + type: object + s3: + description: S3 defines S3-compatible storage as the backup destination. + properties: + bucket: + description: Bucket is the name of the S3 bucket. + type: string + credentialsSecretRef: + description: CredentialsSecretRef references a Secret containing AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY keys. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + type: object + x-kubernetes-map-type: atomic + endpoint: + description: Endpoint is the S3-compatible endpoint URL (e.g., "https://s3.amazonaws.com"). + type: string + forcePathStyle: + description: |- + ForcePathStyle forces path-style S3 URLs (e.g., endpoint/bucket/key) + instead of virtual-hosted-style (e.g., bucket.endpoint/key). + Most S3-compatible providers (MinIO, Ceph) require path style. + type: boolean + key: + description: |- + Key is the key prefix (directory path) within the bucket. + The operator appends the backup filename automatically. + type: string + region: + description: Region is the AWS region for the S3 bucket. + type: string + required: + - bucket + - credentialsSecretRef + - endpoint + type: object + type: object + required: + - clusterRef + - destination + type: object + status: + description: EtcdBackupStatus defines the observed state of EtcdBackup + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current state of this API Resource.\n---\nThis struct is intended for direct use as an array at the field path .status.conditions. For example,\n\n\n\ttype FooStatus struct{\n\t // Represents the observations of a foo's current state.\n\t // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + phase: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/packages/system/etcd-operator/charts/etcd-operator/crds/etcd-cluster.yaml b/packages/system/etcd-operator/charts/etcd-operator/crds/etcd-cluster.yaml index 279e4b63..53bc4f00 100644 --- a/packages/system/etcd-operator/charts/etcd-operator/crds/etcd-cluster.yaml +++ b/packages/system/etcd-operator/charts/etcd-operator/crds/etcd-cluster.yaml @@ -49,6 +49,80 @@ spec: spec: description: EtcdClusterSpec defines the desired state of EtcdCluster properties: + bootstrap: + description: |- + Bootstrap defines initialization from an existing data source (e.g., snapshot restore). + This is only used during initial cluster creation and is ignored afterward. + properties: + restore: + description: Restore configures cluster initialization from an etcd snapshot. + properties: + source: + description: Source defines where to get the snapshot for restoration. + properties: + pvc: + description: PVC defines a PersistentVolumeClaim as the backup destination. + properties: + claimName: + description: ClaimName is the name of the PersistentVolumeClaim to use. + type: string + subPath: + description: |- + SubPath is an optional sub-directory within the PVC volume. + The operator appends the backup filename automatically. + type: string + required: + - claimName + type: object + s3: + description: S3 defines S3-compatible storage as the backup destination. + properties: + bucket: + description: Bucket is the name of the S3 bucket. + type: string + credentialsSecretRef: + description: CredentialsSecretRef references a Secret containing AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY keys. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + type: object + x-kubernetes-map-type: atomic + endpoint: + description: Endpoint is the S3-compatible endpoint URL (e.g., "https://s3.amazonaws.com"). + type: string + forcePathStyle: + description: |- + ForcePathStyle forces path-style S3 URLs (e.g., endpoint/bucket/key) + instead of virtual-hosted-style (e.g., bucket.endpoint/key). + Most S3-compatible providers (MinIO, Ceph) require path style. + type: boolean + key: + description: |- + Key is the key prefix (directory path) within the bucket. + The operator appends the backup filename automatically. + type: string + region: + description: Region is the AWS region for the S3 bucket. + type: string + required: + - bucket + - credentialsSecretRef + - endpoint + type: object + type: object + required: + - source + type: object + type: object headlessServiceTemplate: description: HeadlessService defines the desired state of HeadlessService for etcd members. If not specified, default values will be used. properties: diff --git a/packages/system/etcd-operator/charts/etcd-operator/templates/cert-manager/validatingwebhookconfiguration.yml b/packages/system/etcd-operator/charts/etcd-operator/templates/cert-manager/validatingwebhookconfiguration.yml index 0c2f7c51..4ba1bb67 100644 --- a/packages/system/etcd-operator/charts/etcd-operator/templates/cert-manager/validatingwebhookconfiguration.yml +++ b/packages/system/etcd-operator/charts/etcd-operator/templates/cert-manager/validatingwebhookconfiguration.yml @@ -7,6 +7,46 @@ metadata: {{- include "etcd-operator.labels" . | nindent 4 }} name: {{ include "etcd-operator.fullname" . }}-validating-webhook-configuration webhooks: + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "etcd-operator.fullname" . }}-webhook-service + namespace: {{ .Release.Namespace }} + path: /validate-etcd-aenix-io-v1alpha1-etcdbackup + failurePolicy: Fail + name: vetcdbackup.kb.io + rules: + - apiGroups: + - etcd.aenix.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - etcdbackups + sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "etcd-operator.fullname" . }}-webhook-service + namespace: {{ .Release.Namespace }} + path: /validate-etcd-aenix-io-v1alpha1-etcdbackupschedule + failurePolicy: Fail + name: vetcdbackupschedule.kb.io + rules: + - apiGroups: + - etcd.aenix.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - etcdbackupschedules + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/packages/system/etcd-operator/charts/etcd-operator/templates/rbac/clusterrole-manager-role.yml b/packages/system/etcd-operator/charts/etcd-operator/templates/rbac/clusterrole-manager-role.yml index 825c726c..6d17d7f3 100644 --- a/packages/system/etcd-operator/charts/etcd-operator/templates/rbac/clusterrole-manager-role.yml +++ b/packages/system/etcd-operator/charts/etcd-operator/templates/rbac/clusterrole-manager-role.yml @@ -74,6 +74,76 @@ rules: - get - list - watch + - apiGroups: + - batch + resources: + - cronjobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - batch + resources: + - jobs + verbs: + - create + - delete + - get + - list + - watch + - apiGroups: + - etcd.aenix.io + resources: + - etcdbackups + verbs: + - get + - list + - patch + - update + - watch + - apiGroups: + - etcd.aenix.io + resources: + - etcdbackups/finalizers + verbs: + - update + - apiGroups: + - etcd.aenix.io + resources: + - etcdbackups/status + verbs: + - get + - patch + - update + - apiGroups: + - etcd.aenix.io + resources: + - etcdbackupschedules + verbs: + - get + - list + - patch + - update + - watch + - apiGroups: + - etcd.aenix.io + resources: + - etcdbackupschedules/finalizers + verbs: + - update + - apiGroups: + - etcd.aenix.io + resources: + - etcdbackupschedules/status + verbs: + - get + - patch + - update - apiGroups: - etcd.aenix.io resources: diff --git a/packages/system/etcd-operator/charts/etcd-operator/templates/workload/deployment.yml b/packages/system/etcd-operator/charts/etcd-operator/templates/workload/deployment.yml index 206860b8..dbaa029f 100644 --- a/packages/system/etcd-operator/charts/etcd-operator/templates/workload/deployment.yml +++ b/packages/system/etcd-operator/charts/etcd-operator/templates/workload/deployment.yml @@ -66,6 +66,8 @@ spec: fieldRef: apiVersion: v1 fieldPath: metadata.namespace + - name: OPERATOR_IMAGE + value: {{ .Values.etcdOperator.image.repository }}:{{ .Values.etcdOperator.image.tag | default .Chart.AppVersion }} volumeMounts: - mountPath: /tmp/k8s-webhook-server/serving-certs name: cert diff --git a/packages/system/etcd-rd/cozyrds/etcd.yaml b/packages/system/etcd-rd/cozyrds/etcd.yaml index 7ff7c39c..912b1dc1 100644 --- a/packages/system/etcd-rd/cozyrds/etcd.yaml +++ b/packages/system/etcd-rd/cozyrds/etcd.yaml @@ -8,7 +8,7 @@ spec: plural: etcds singular: etcd openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"size":{"description":"Persistent Volume size.","default":"4Gi","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":""},"replicas":{"description":"Number of etcd replicas.","type":"integer","default":3},"resources":{"description":"Resource configuration for etcd.","type":"object","default":{},"properties":{"cpu":{"description":"Number of CPU cores allocated.","default":"1000m","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":"Amount of memory allocated.","default":"512Mi","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}}}}} + {"title":"Chart Values","type":"object","properties":{"size":{"description":"Persistent Volume size.","default":"4Gi","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":""},"replicas":{"description":"Number of etcd replicas.","type":"integer","default":3},"resources":{"description":"Resource configuration for etcd.","type":"object","default":{},"properties":{"cpu":{"description":"Number of CPU cores allocated.","default":"1000m","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":"Amount of memory allocated.","default":"512Mi","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}}},"backup":{"description":"Backup configuration.","type":"object","default":{},"properties":{"destinationPath":{"description":"Destination path for backups (e.g. s3://bucket/path/).","type":"string","default":""},"enabled":{"description":"Enable scheduled S3 backups.","type":"boolean","default":false},"endpointURL":{"description":"S3 endpoint URL for uploads.","type":"string","default":""},"failedJobsHistoryLimit":{"description":"Number of failed backup jobs to retain.","type":"integer","default":1},"forcePathStyle":{"description":"Use path-style S3 URLs (required for MinIO and most S3-compatible providers).","type":"boolean","default":true},"region":{"description":"S3 region.","type":"string","default":""},"s3AccessKey":{"description":"Access key for S3 authentication.","type":"string","default":""},"s3SecretKey":{"description":"Secret key for S3 authentication.","type":"string","default":""},"schedule":{"description":"Cron schedule for automated backups.","type":"string","default":"0 2 * * *"},"successfulJobsHistoryLimit":{"description":"Number of successful backup jobs to retain.","type":"integer","default":3}}}}} release: prefix: "" labels: @@ -26,7 +26,7 @@ spec: description: Storage for Kubernetes clusters module: true icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9Ii0wLjAwMTk1MzEyIiB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgcng9IjI0IiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfNjgzXzI5NjMpIi8+CjxwYXRoIGQ9Ik0xMjIuNDQyIDczLjQ3MjlDMTIxLjk1OSA3My41MTM0IDEyMS40NzQgNzMuNTMyMiAxMjAuOTU4IDczLjUzMjJDMTE3Ljk2NSA3My41MzIyIDExNS4wNjEgNzIuODMwNCAxMTIuNDQyIDcxLjU0NTFDMTEzLjMxNCA2Ni41NDIxIDExMy42ODUgNjEuNTAxOSAxMTMuNTg4IDU2LjQ4MDJDMTEwLjc0OCA1Mi4zNzIzIDEwNy41MDIgNDguNDk2NSAxMDMuODM4IDQ0LjkyNTdDMTA1LjQyOCA0MS45NDU0IDEwNy43NzggMzkuMzgxMSAxMTAuNzExIDM3LjU2MjhMMTExLjk3MSAzNi43ODQyTDExMC45ODkgMzUuNjc3NEMxMDUuOTMyIDI5Ljk4MzIgOTkuODk3MSAyNS41ODA5IDkzLjA1NDcgMjIuNTkzN0w5MS42OTAyIDIyTDkxLjM0MzcgMjMuNDQyM0M5MC41Mjc3IDI2LjgwMzYgODguODIyMiAyOS44MzYgODYuNDgwNyAzMi4yNjk1QzgxLjk4MDMgMjkuODc3NCA3Ny4yNzg4IDI3Ljk0NCA3Mi40MzA1IDI2LjQ3OTdDNjcuNTkzNyAyNy45NDA4IDYyLjkwMDUgMjkuODY4NiA1OC40MDIgMzIuMjU3MUM1Ni4wNzAxIDI5LjgyNjggNTQuMzY4OCAyNi44MDE4IDUzLjU1NiAyMy40NTAxTDUzLjIwNzIgMjIuMDA4M0w1MS44NDc3IDIyLjU5OTJDNDUuMDkxNCAyNS41NDMxIDM4Ljg5MDEgMzAuMDY0NyAzMy45MTYyIDM1LjY3NDJMMzIuOTMxOCAzNi43ODMzTDM0LjE5IDM3LjU2MTlDMzcuMTE0MiAzOS4zNzMzIDM5LjQ1NzYgNDEuOTIyNCA0MS4wNDQ0IDQ0Ljg4NjZDMzcuMzkxNyA0OC40NDM1IDM0LjE0OTUgNTIuMzA3IDMxLjMxMTkgNTYuMzk1OUMzMS4yMDE0IDYxLjQxNTQgMzEuNTUzNSA2Ni40OTI0IDMyLjQyOTcgNzEuNTY0NEMyOS44MjMxIDcyLjgzNzggMjYuOTM1OCA3My41MzE4IDIzLjk2MjggNzMuNTMxOEMyMy40NDA5IDczLjUzMTggMjIuOTUyNyA3My41MTI5IDIyLjQ3ODIgNzMuNDczM0wyMSA3My4zNjA2TDIxLjEzODUgNzQuODM2NUMyMS44NjI5IDgyLjMwMzMgMjQuMTgxNCA4OS40MDUzIDI4LjAzMzQgOTUuOTQ3MUwyOC43ODUzIDk3LjIyMzdMMjkuOTE0MiA5Ni4yNjU2QzMyLjUzMDUgOTQuMDQ2NSAzNS42OTE3IDkyLjU3NyAzOS4wNTMgOTEuOTg0N0M0MS4yNjg5IDk2LjUxNTUgNDMuODk1MyAxMDAuNzcyIDQ2Ljg3NDcgMTA0LjcyNUM1MS42Mjg3IDEwNi4zODcgNTYuNTgxOSAxMDcuNjI5IDYxLjY5NzEgMTA4LjM2N0M2Mi4xODc3IDExMS43NSA2MS43OTcgMTE1LjI0OSA2MC40NjI0IDExOC40ODRMNTkuODk5NSAxMTkuODU1TDYxLjM0NjkgMTIwLjE3NEM2NS4wNTI5IDEyMC45ODkgNjguNzkxNyAxMjEuNDA0IDcyLjQ1MjYgMTIxLjQwNEw4My41NTUxIDEyMC4xNzRMODUuMDAzOSAxMTkuODU1TDg0LjQzOTcgMTE4LjQ4MkM4My4xMDg3IDExNS4yNDYgODIuNzE4IDExMS43NDMgODMuMjA4NiAxMDguMzZDODguMzAzNiAxMDcuNjIxIDkzLjIzODQgMTA2LjM4MiA5Ny45NzQ4IDEwNC43MjVDMTAwLjk1NyAxMDAuNzY5IDEwMy41ODYgOTYuNTA5NSAxMDUuODA1IDkxLjk3MjhDMTA5LjE3NyA5Mi41NjE0IDExMi4zNTYgOTQuMDMxNyAxMTQuOTg5IDk2LjI1NzNMMTE2LjExOCA5Ny4yMTQxTDExNi44NjYgOTUuOTQwN0MxMjAuNzI1IDg5LjM5MDUgMTIzLjA0MyA4Mi4yODkxIDEyMy43NTYgNzQuODM0MkwxMjMuODk1IDczLjM2MUwxMjIuNDQyIDczLjQ3MjlaTTg4LjMxOTcgOTEuNTE4MUM4My4wNjczIDkyLjk0NjYgNzcuNzMzIDkzLjY2NzcgNzIuNDMwNSA5My42Njc3QzY3LjExMzcgOTMuNjY3NyA2MS43ODU5IDkyLjk0NyA1Ni41MjkgOTEuNTE4MUM1My42NDQ4IDg3LjAzNjYgNTEuMzY0NSA4Mi4yMzU3IDQ5LjcyMzQgNzcuMTgxMkM0OC4wODkyIDcyLjE1MDIgNDcuMTMyOSA2Ni44Nzk1IDQ2Ljg1NTQgNjEuNDUyMkM1MC4yNTA0IDU3LjI1NDcgNTQuMTExIDUzLjU3NzYgNTguMzc2NyA1MC40ODIzQzYyLjcxMTQgNDcuMzI5NCA2Ny40MjcxIDQ0Ljc2NzkgNzIuNDMwNSA0Mi44NDFDNzcuNDI1NiA0NC43NjgzIDgyLjEzMjYgNDcuMzI2MiA4Ni40NTcyIDUwLjQ2NTdDOTAuNzM5NCA1My41Nzc2IDk0LjYxNzEgNTcuMjgzMiA5OC4wMjg3IDYxLjUwN0M5Ny43Mzc4IDY2LjkwMzQgOTYuNzcgNzIuMTQzOCA5NS4xMzMgNzcuMTY2NUM5My40OTYxIDgyLjIyIDkxLjIwODQgODcuMDM2MSA4OC4zMTk3IDkxLjUxODFaTTc2Ljc2ODQgNjYuMTk3NEM3Ni43Njg0IDY5LjkwODEgNzkuNzc1NCA3Mi45MDk2IDgzLjQ4MSA3Mi45MDk2Qzg3LjE4NTcgNzIuOTA5NiA5MC4xODk1IDY5LjkwODYgOTAuMTg5NSA2Ni4xOTc0QzkwLjE4OTUgNjIuNTAxIDg3LjE4NTcgNTkuNDg4MSA4My40ODEgNTkuNDg4MUM3OS43NzU0IDU5LjQ4ODEgNzYuNzY4NCA2Mi41MDEgNzYuNzY4NCA2Ni4xOTc0Wk02OC4wOTU0IDY2LjE5NzRDNjguMDk1NCA2OS45MDgxIDY1LjA4ODggNzIuOTA5NiA2MS4zODMyIDcyLjkwOTZDNTcuNjc0OSA3Mi45MDk2IDU0LjY3NjYgNjkuOTA4NiA1NC42NzY2IDY2LjE5NzRDNTQuNjc2NiA2Mi41MDI0IDU3LjY3NTMgNTkuNDg5NCA2MS4zODMyIDU5LjQ4OTRDNjUuMDg4OCA1OS40ODk0IDY4LjA5NTQgNjIuNTAyNCA2OC4wOTU0IDY2LjE5NzRaIiBmaWxsPSJ3aGl0ZSIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzY4M18yOTYzIiB4MT0iNS41IiB5MT0iMTEiIHgyPSIxNDEiIHkyPSIxMjQuNSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjNTNCMkYwIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzQxOUVEQSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo= - keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "size"], ["spec", "storageClass"], ["spec", "replicas"], ["spec", "resources"], ["spec", "resources", "cpu"], ["spec", "resources", "memory"]] + keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "size"], ["spec", "storageClass"], ["spec", "replicas"], ["spec", "resources"], ["spec", "resources", "cpu"], ["spec", "resources", "memory"], ["spec", "backup"], ["spec", "backup", "enabled"], ["spec", "backup", "schedule"], ["spec", "backup", "destinationPath"], ["spec", "backup", "endpointURL"], ["spec", "backup", "region"], ["spec", "backup", "forcePathStyle"], ["spec", "backup", "s3AccessKey"], ["spec", "backup", "s3SecretKey"], ["spec", "backup", "successfulJobsHistoryLimit"], ["spec", "backup", "failedJobsHistoryLimit"]] secrets: exclude: [] include: []