Compare commits

...
Sign in to create a new pull request.

16 commits

Author SHA1 Message Date
Aleksei Sviridkin
cc7d09b458
fix(tests): use far-future cron in etcd backup e2e to avoid job churn
Address review feedback from coderabbitai on
hack/e2e-apps/etcd.bats:89: the test verifies that the chart renders
EtcdBackupSchedule and that the etcd-operator materializes a CronJob,
not that backups fire. Use "0 0 1 1 *" so no Jobs run against the
intentionally invalid endpoint during the ~6-minute test window.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-24 13:44:21 +03:00
Aleksei Sviridkin
97eef022e5
fix(tests): poll for etcd HelmRelease existence before waiting on readiness
Address review feedback from coderabbitai on
hack/e2e-apps/etcd.bats:39-40: replace the sleep-then-wait pattern with
a wait_etcd_hr_ready helper that polls hr/etcd until the Cozystack API
reconciles the Etcd CR and only then waits for condition=ready.
kubectl wait exits immediately when the resource does not yet exist,
which caused spurious failures when HelmRelease creation took longer
than 5s.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-24 13:44:02 +03:00
Aleksei Sviridkin
86a93af283
fix(tests): stop masking etcd e2e cleanup failures
Address review feedback from coderabbitai on
hack/e2e-apps/etcd.bats:9-14: drop the '|| true' guards in setup() so
real teardown failures (RBAC errors, stuck finalizers, wait timeouts)
fail the test setup instead of letting the next test run against stale
HelmRelease, Secret, or EtcdBackupSchedule state. --ignore-not-found
still covers the empty-namespace case on a clean run.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-24 13:43:23 +03:00
Aleksei Sviridkin
040d5d4681
docs(etcd): use "custom resource" instead of "CRD" for EtcdBackup
Address review feedback from coderabbitai on
packages/extra/etcd/README.md:9: CRDs are schema definitions; what users
apply to trigger a restore or one-shot backup is a custom resource
instance of the EtcdBackup kind, not the CRD itself. Reword accordingly.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-24 13:43:05 +03:00
Aleksei Sviridkin
f8f3b29480
fix(tests): correct stderr redirection order in etcd diagnostics
Address review feedback from coderabbitai on
hack/e2e-apps/etcd.bats:19-21: redirect stdout to fd 3 first, then dup
stderr onto the new fd 1 so failures from kubectl (NotFound, RBAC,
timeouts) reach the test diagnostics stream instead of being swallowed
by the bats-captured default stdout.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-24 13:42:37 +03:00
Aleksei Sviridkin
aaf9dd047c
fix(etcd): cast backup history limits to int in template
Address review feedback from gemini-code-assist on
packages/extra/etcd/templates/etcd-backup-schedule.yaml:20-21: explicitly
coerce successfulJobsHistoryLimit and failedJobsHistoryLimit to int so
string values passed via helm --set do not produce a type mismatch in
the EtcdBackupSchedule spec.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-24 13:42:17 +03:00
Aleksei Sviridkin
4d7b4bbb33
fix(tests): replace bats run idiom in etcd e2e test
cozytest.sh does not implement the bats 'run <cmd>' helper and '$status'
variable, so the 'Create Etcd with empty backup block (disabled by
default)' test died with 'run: not found' on every attempt. Replace the
bats idiom with plain shell: use '!' prefix and redirect stderr to
assert the resources absent-when-backup-disabled invariant directly.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-21 11:03:27 +03:00
Aleksei Sviridkin
9fc99eed50
chore(etcd): regenerate values.schema.json and cozyrd manifest
Run 'make generate' under packages/extra/etcd to refresh the JSON schema
and the corresponding ResourceDefinition YAML after adding backup and
resources fields. Matches cozyvalues-gen v1.3.0 output used by the
pre-commit CI check.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-21 02:30:27 +03:00
Aleksei Sviridkin
6deca1b07d
chore(etcd-operator): opt into CreateReplace upgrade policy for CRDs
v0.4.3 introduced new CRDs (EtcdBackup, EtcdBackupSchedule). Flux defaults to Skip on upgrade, so on existing clusters those CRDs never land without a manual kubectl apply.

Setting upgradeCRDs: CreateReplace on the etcd-operator component tells PackageReconciler to wire hr.Spec.Upgrade.Crds accordingly. etcd-operator's CRDs evolve additively between versions, so CreateReplace is safe here.

Depends on the PackageSource.Spec.Variants[].Components[].Install.UpgradeCRDs field — must ship with a cozystack-operator release that recognizes it. Without that, this field is ignored and the HelmRelease still defaults to Skip.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-20 18:59:21 +03:00
Aleksei Sviridkin
ed77e9c6e3
fix(etcd): add helm unit tests, simplify backup guards, harden e2e
Fourth review round fixes.

Simplify backup validation by dropping placeholder-regex guards and switching default values of s3AccessKey, s3SecretKey, destinationPath, and endpointURL from angle-bracketed hints to empty strings. A single 'required' check on each field is now the whole validation for presence; the regex guard was brittle (silently stopped matching if the hint ever changed) and dropping it makes the values.yaml defaults match the behavior contract the README describes. s3:// prefix and '//' rejection still live in etcd-backup-schedule.yaml as template-time fail branches.

Add helm-unittest suites in packages/extra/etcd/tests/ covering both templates end-to-end: rendered output assertions for the happy path (bucket/key split, region passthrough, conditional rendering of optional fields) and failedTemplate assertions for every fail branch. A 'test' target is wired into the package Makefile so hack/helm-unit-tests.sh picks it up on 'make unit-tests'.

Add type: Opaque to backup-secret.yaml for consistency with the mongodb template.

E2E: wait for HelmRelease, Secret, and EtcdBackupSchedule deletion in setup() so the empty-backup case does not flake against lingering state from the previous test. Replace the fictitious minio-e2e hostname with no-such-endpoint.invalid and explicitly document that the e2e verifies schedule/CronJob materialization, not backup execution.

Restore via EtcdCluster.spec.bootstrap and the one-shot EtcdBackup CRD are still not exposed through this chart; both are explicit follow-ups called out in the README.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-20 17:01:58 +03:00
Aleksei Sviridkin
12fc9cd415
fix(etcd): address third review round
- Use name='etcd' in all three e2e cases. The chart pins the Helm
  release name to 'etcd' via check-release-name.yaml, so any other
  name causes template rendering to fail before the HelmRelease
  can reconcile. This is why the tenant chart hardcodes HR name
  'etcd' in packages/apps/tenant/templates/etcd.yaml.

- Consolidate all backup validation guards into
  etcd-backup-schedule.yaml so Helm's alphabetical render order no
  longer determines which failure a user sees first. Previously, a
  bad destinationPath would be masked by the earlier placeholder
  check in backup-secret.yaml.

- Use regex match '^<.*>$' for placeholder detection instead of
  exact string equality. The previous check silently stopped
  firing if anyone updated the default placeholder string in
  values.yaml; the regex form catches any angle-bracketed default.

- Reject destinationPath strings containing '//' after the s3://
  prefix. Previously 's3://bucket//subdir' silently produced a key
  beginning with '/', confusing S3 clients and retention policies.

- Document the backup contract in README.md: what enabling it
  produces, which fields are required, why placeholders are
  rejected, and that restore is a known follow-up gap.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-20 16:37:26 +03:00
Aleksei Sviridkin
561bc4dc23
fix(etcd): address second review round
- Mark backup.enabled as optional in the values typedef so
  generated JSON schema no longer lists it in 'required'.
  Previously, 'backup: {}' (relying on the enabled:false default)
  failed schema validation at CRD admission.

- Validate backup.destinationPath has s3:// prefix. Without this
  check, passing a path without the scheme silently used the
  first path segment as the bucket name.

- Add tail newline to values.schema.json to match repo convention
  (cozyvalues-gen output lacks it — worth fixing in that project
  separately).

- Add bats setup() that clears tenant-test etcd resources before
  each test so a failure mid-test does not leak into subsequent
  tests. Hardcoded chart-level resource names ('etcd',
  'etcd-s3-creds') make two Etcd apps in the same namespace
  collide; setup() enforces a single owner per test.

- Add dump_diagnostics() helper that dumps operator logs + the
  relevant CRD state on wait-for-ready timeouts, so CI failures
  surface actionable context instead of bare exit code 124.

- Add a third e2e case 'Create Etcd with empty backup block' that
  asserts backup:{} admits and produces no backup resources — a
  regression test for the required:['enabled'] bug fixed here.

Known scope: the upstream EtcdCluster.spec.bootstrap restore field
(also new in v0.4.3) is not exposed through this values.yaml yet.
Restoring a cluster from an existing snapshot is a follow-up PR.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-20 16:15:56 +03:00
Aleksei Sviridkin
0e03ed9ad4
fix(etcd): reject placeholder backup values and harden e2e
Review found three issues:

1. The guard in backup-secret.yaml only triggered on empty strings,
   not on the placeholder defaults ('<your-access-key>' etc). A user
   enabling backup.enabled=true without overriding the values would
   produce a Secret containing the literal placeholder, and the
   backup CronJob would fail at runtime with an opaque 403 far from
   the source. Add explicit template guards that fail rendering when
   any of the four required fields still hold their placeholder
   default.

2. The backup e2e test waited on .spec.schedule of the newly-created
   EtcdBackupSchedule, which is set at apply time — the wait was
   tautological and did not prove the etcd-operator reconciled
   anything. Replace it with a wait on the EtcdCluster's Ready
   condition plus a poll for the CronJob the operator generates
   from the schedule (selected via the
   etcd.aenix.io/etcdbackupschedule-name label).

3. The backup e2e test also did not wait for EtcdCluster readiness
   before tearing down. Add the same Ready wait as the baseline
   test, and verify the Secret contains the real access key value
   rather than just existing.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-20 16:08:43 +03:00
Aleksei Sviridkin
ee8ceecc0e
test(etcd): add e2e for etcd application and backup schedule
Adds two bats cases: a baseline Etcd managed-app deployment and a
deployment with backup.enabled=true that verifies the
EtcdBackupSchedule resource and the S3 credentials Secret are
materialized. The backup case uses fake S3 credentials — the
operator will not successfully push backups, but the schedule
resource itself is reconciled, which is what this test validates.

Also hardcodes the EtcdBackupSchedule and backup Secret names to
'etcd' / 'etcd-s3-creds' to match the existing hardcoded
EtcdCluster name in the chart (one etcd per tenant namespace).

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-20 15:58:55 +03:00
Aleksei Sviridkin
51c8192e1e
feat(etcd): add S3 backup schedule support
Expose the EtcdBackupSchedule CRD (etcd.aenix.io/v1alpha1) through
optional values so operators can declaratively configure periodic
S3 backups of the platform etcd cluster. Kamaji stores tenant
control-plane etcd data here, so losing this cluster means losing
every tenant cluster — a backup schedule is critical.

The backup is disabled by default (backup.enabled: false), keeping
existing installations unchanged. When enabled, a Secret is created
with AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY keys referenced by
the new EtcdBackupSchedule resource. Field naming mirrors the
existing postgres backup pattern (destinationPath, endpointURL,
s3AccessKey, s3SecretKey, schedule) for consistency.

Requires etcd-operator v0.4.3+ (shipped in the previous commit).

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-20 15:57:51 +03:00
Aleksei Sviridkin
8734c38da3
chore(etcd-operator): update vendored chart to v0.4.3
Upstream release notes: https://github.com/aenix-io/etcd-operator/releases/tag/v0.4.3

Notable changes:
- Add EtcdBackup CRD for declarative etcd snapshots (#307)
- Add EtcdBackupSchedule CRD for scheduled backups (#307)
- Expose metrics port on client service (#299)
- Replace deprecated gcr.io/kubebuilder/kube-rbac-proxy with
  quay.io/brancz/kube-rbac-proxy:v0.18.1 (#308)
- Update Kamaji example manifest

Note: Helm 3 does not install CRDs on upgrade. Existing clusters
require manual application of the new EtcdBackup and
EtcdBackupSchedule CRDs:

  kubectl apply --filename packages/system/etcd-operator/charts/etcd-operator/crds/

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-20 15:53:41 +03:00
18 changed files with 1175 additions and 4 deletions

113
hack/e2e-apps/etcd.bats Normal file
View file

@ -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- <<EOF
apiVersion: apps.cozystack.io/v1alpha1
kind: Etcd
metadata:
name: etcd
namespace: tenant-test
spec:
size: 1Gi
replicas: 3
storageClass: ""
resources:
cpu: 100m
memory: 128Mi
EOF
wait_etcd_hr_ready || { dump_diagnostics; false; }
kubectl -n tenant-test wait etcdcluster.etcd.aenix.io etcd --timeout=180s --for=jsonpath='{.status.conditions[?(@.type=="Ready")].status}'=True || { dump_diagnostics; false; }
}
@test "Create Etcd with empty backup block (disabled by default)" {
kubectl apply -f- <<EOF
apiVersion: apps.cozystack.io/v1alpha1
kind: Etcd
metadata:
name: etcd
namespace: tenant-test
spec:
size: 1Gi
replicas: 3
storageClass: ""
resources:
cpu: 100m
memory: 128Mi
backup: {}
EOF
wait_etcd_hr_ready || { dump_diagnostics; false; }
# With backup disabled, neither the schedule nor the secret should be created.
! kubectl -n tenant-test get etcdbackupschedule.etcd.aenix.io etcd 2>/dev/null
! kubectl -n tenant-test get secret etcd-s3-creds 2>/dev/null
}
@test "Create Etcd with backup schedule" {
kubectl apply -f- <<EOF
apiVersion: apps.cozystack.io/v1alpha1
kind: Etcd
metadata:
name: etcd
namespace: tenant-test
spec:
size: 1Gi
replicas: 3
storageClass: ""
resources:
cpu: 100m
memory: 128Mi
backup:
enabled: true
# Schedule is chosen far in the future so no Jobs fire during the
# ~6-minute test window; this test only checks that the chart renders
# EtcdBackupSchedule/Secret and that the etcd-operator materializes a
# CronJob — it does NOT verify that backups reach S3. The endpoint
# below intentionally resolves nowhere to keep the test self-contained.
schedule: "0 0 1 1 *"
destinationPath: "s3://test-bucket/etcd-backups/"
endpointURL: "http://no-such-endpoint.invalid:9000"
region: "us-east-1"
forcePathStyle: true
s3AccessKey: "e2e-access-key"
s3SecretKey: "e2e-secret-key"
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 1
EOF
wait_etcd_hr_ready || { dump_diagnostics; false; }
kubectl -n tenant-test wait etcdcluster.etcd.aenix.io etcd --timeout=180s --for=jsonpath='{.status.conditions[?(@.type=="Ready")].status}'=True || { dump_diagnostics; false; }
kubectl -n tenant-test get etcdbackupschedule.etcd.aenix.io etcd || { dump_diagnostics; false; }
# Verify the region field propagated to the EtcdBackupSchedule.
REGION=$(kubectl -n tenant-test get etcdbackupschedule.etcd.aenix.io etcd -o jsonpath='{.spec.destination.s3.region}')
[ "$REGION" = "us-east-1" ]
kubectl -n tenant-test get secret etcd-s3-creds -o jsonpath='{.data.AWS_ACCESS_KEY_ID}' | base64 -d | grep -q '^e2e-access-key$'
# The etcd-operator generates a CronJob from the EtcdBackupSchedule. Wait for it.
timeout 120 sh -ec "until [ \"\$(kubectl -n tenant-test get cronjob -l etcd.aenix.io/etcdbackupschedule-name=etcd -o jsonpath='{.items[*].metadata.name}' 2>/dev/null)\" != '' ]; do sleep 5; done" || { dump_diagnostics; false; }
}

View file

@ -21,3 +21,4 @@ spec:
install:
namespace: cozy-etcd-operator
releaseName: etcd-operator
upgradeCRDs: CreateReplace

View file

@ -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 .

View file

@ -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` |

View file

@ -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 }}

View file

@ -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 }}

View file

@ -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"

View file

@ -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/)"

View file

@ -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
}
}
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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: {}

View file

@ -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: {}

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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

View file

@ -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: []