From 8810bab43e72763e841414b03920ef7a517dcb48 Mon Sep 17 00:00:00 2001 From: Austin Barrington Date: Thu, 22 Oct 2026 23:57:20 +0100 Subject: [PATCH] Back up with the AWS CLI; build on Go 1.27.3 (CVE-2026-111943) CVE-2026-111943: eight eMudhra intermediates under emSign ECC Root CA - G3 have trivially recoverable keys. - The operator image (0.3.2) trusts that root: its distroless base predates the fix. Rebuilt on the current base it does not. Go 1.25 gets no fix (only 1.26.10 and 1.27.3 block the intermediates in crypto/x509) and is out of support: the builder and CI move to Go 1.27.3. - Backup and restore Jobs used minio/mc, which is gone from Docker Hub (quay.io/minio needs a login): a Job on a node without it cached could not start. They now use amazon/aws-cli:2.38.5, whose botocore CA bundle no longer holds the root. A custom endpoint (MinIO, other S3-compatible stores) is addressed path-style; AWS by region; credentials still come from the same Secret, or the default AWS chain without one. Retention deletes the timestamped backup folders older than the cutoff, by name; restore downloads only the newest backup instead of the whole prefix. Checked end to end against a local S3 (rclone serve s3): upload, a 2020 backup removed, a non-backup folder kept, the newest backup restored. golangci-lint v2.8.0 cannot read Go 1.27 export data: v2.15.0. Its goconst no longer skips tests; test files are excluded again, and the six repeated literals it found in non-test code are constants now. The deprecated Result.Requeue on a StatefulSet update conflict is RequeueAfter: 1s. --- .custom-gcl.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/test-chart.yml | 2 +- .github/workflows/test-e2e.yml | 2 +- .github/workflows/test.yml | 2 +- .golangci.yml | 5 + Dockerfile | 2 +- Makefile | 2 +- internal/controller/backup_job_test.go | 55 ++++++++--- .../hyperbytedbbackup_controller.go | 2 +- .../hyperbytedbcluster_controller.go | 11 ++- .../hyperbytedbrestore_controller.go | 2 +- internal/controller/s3_transfer.go | 94 +++++++++++++++---- internal/hyperbytedb/compat.go | 24 +++-- internal/hyperbytedb/statefulset.go | 18 ++-- 15 files changed, 165 insertions(+), 60 deletions(-) diff --git a/.custom-gcl.yml b/.custom-gcl.yml index 6ef11b3..6effe6a 100644 --- a/.custom-gcl.yml +++ b/.custom-gcl.yml @@ -3,7 +3,7 @@ # with all the plugins listed below. # # See: https://golangci-lint.run/plugins/module-plugins/ -version: v2.8.0 +version: v2.15.0 plugins: # logcheck validates structured logging calls and parameters (e.g., balanced key-value pairs) - module: "sigs.k8s.io/logtools" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9790a3a..75da691 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.25" + go-version: "1.27.3" cache-dependency-path: go.sum # Checkout files are owned by the runner user. Without marking the workspace diff --git a/.github/workflows/test-chart.yml b/.github/workflows/test-chart.yml index 20333a8..d44eb26 100644 --- a/.github/workflows/test-chart.yml +++ b/.github/workflows/test-chart.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.25" + go-version: "1.27.3" cache-dependency-path: go.sum - name: Install CI dependencies diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index a2f767a..b30d44d 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -24,7 +24,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.25" + go-version: "1.27.3" cache-dependency-path: go.sum - name: Install CI dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f61d553..231139c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.25" + go-version: "1.27.3" cache-dependency-path: go.sum # Required so that `go list`/`go test` can run `git` for VCS stamping diff --git a/.golangci.yml b/.golangci.yml index f966bb1..0197a30 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -45,6 +45,11 @@ linters: - dupl - lll path: internal/* + # goconst stopped ignoring tests (golangci-lint v2.8 -> v2.15); repeated + # literals in table tests are clearer as they are. + - linters: + - goconst + path: _test\.go paths: - third_party$ - builtin$ diff --git a/Dockerfile b/Dockerfile index 4b44e7c..b281d30 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 # Build the manager binary -FROM golang:1.25 AS builder +FROM golang:1.27.3 AS builder ARG TARGETOS ARG TARGETARCH diff --git a/Makefile b/Makefile index 09c6eed..c5c895e 100644 --- a/Makefile +++ b/Makefile @@ -200,7 +200,7 @@ ENVTEST_K8S_VERSION ?= $(shell v='$(call gomodver,k8s.io/api)'; \ [ -n "$$v" ] || { echo "Set ENVTEST_K8S_VERSION manually (k8s.io/api replace has no tag)" >&2; exit 1; }; \ printf '%s\n' "$$v" | sed -E 's/^v?[0-9]+\.([0-9]+).*/1.\1/') -GOLANGCI_LINT_VERSION ?= v2.8.0 +GOLANGCI_LINT_VERSION ?= v2.15.0 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. $(KUSTOMIZE): $(LOCALBIN) diff --git a/internal/controller/backup_job_test.go b/internal/controller/backup_job_test.go index 9faa97f..1ab8199 100644 --- a/internal/controller/backup_job_test.go +++ b/internal/controller/backup_job_test.go @@ -39,7 +39,7 @@ func testBackup() *hyperbytedbv1alpha1.HyperbytedbBackup { } } -func TestBuildBackupJobSnapshotsThenUploadsWithMc(t *testing.T) { +func TestBuildBackupJobSnapshotsThenUploadsWithAwsCli(t *testing.T) { r := &HyperbytedbBackupReconciler{} job := r.buildBackupJob(testBackup(), testBackupCluster(), "db-test-backup-abcd") spec := job.Spec.Template.Spec @@ -63,21 +63,22 @@ func TestBuildBackupJobSnapshotsThenUploadsWithMc(t *testing.T) { } if len(spec.Containers) != 1 { - t.Fatalf("containers = %d, want 1 (mc upload)", len(spec.Containers)) + t.Fatalf("containers = %d, want 1 (S3 upload)", len(spec.Containers)) } up := spec.Containers[0] if up.Name != "upload" { t.Errorf("container name = %q, want upload", up.Name) } - if !strings.Contains(up.Image, "minio/mc") { - t.Errorf("upload image = %q, want minio/mc", up.Image) + // minio/mc is no longer pullable from Docker Hub. + if !strings.HasPrefix(up.Image, "amazon/aws-cli:") { + t.Errorf("upload image = %q, want amazon/aws-cli", up.Image) } upScript := strings.Join(up.Command, " ") - if !strings.Contains(upScript, "mc ") { - t.Errorf("upload command missing mc: %s", upScript) + if !strings.Contains(upScript, "aws $EP s3 sync /snapshot") { + t.Errorf("upload command missing aws s3 sync: %s", upScript) } - if strings.Contains(upScript, "aws ") { - t.Errorf("upload must not call aws: %s", upScript) + if strings.Contains(upScript, "mc ") { + t.Errorf("upload must not call mc: %s", upScript) } names := map[string]bool{} @@ -89,7 +90,7 @@ func TestBuildBackupJobSnapshotsThenUploadsWithMc(t *testing.T) { } } -func TestBuildRestoreJobDownloadsWithMcThenRestores(t *testing.T) { +func TestBuildRestoreJobDownloadsWithAwsCliThenRestores(t *testing.T) { r := &HyperbytedbRestoreReconciler{} restore := &hyperbytedbv1alpha1.HyperbytedbRestore{ ObjectMeta: metav1.ObjectMeta{Name: "db-test-restore-abcd", Namespace: "tenant-1"}, @@ -108,17 +109,17 @@ func TestBuildRestoreJobDownloadsWithMcThenRestores(t *testing.T) { spec := job.Spec.Template.Spec if len(spec.InitContainers) != 1 { - t.Fatalf("init containers = %d, want 1 (mc download)", len(spec.InitContainers)) + t.Fatalf("init containers = %d, want 1 (S3 download)", len(spec.InitContainers)) } dl := spec.InitContainers[0] if dl.Name != "download" { t.Errorf("init name = %q, want download", dl.Name) } - if !strings.Contains(dl.Image, "minio/mc") { - t.Errorf("download image = %q, want minio/mc", dl.Image) + if !strings.HasPrefix(dl.Image, "amazon/aws-cli:") { + t.Errorf("download image = %q, want amazon/aws-cli", dl.Image) } - if strings.Contains(strings.Join(dl.Command, " "), "aws ") { - t.Errorf("download must not call aws: %s", dl.Command) + if !strings.Contains(strings.Join(dl.Command, " "), "aws $EP s3 sync") { + t.Errorf("download command missing aws s3 sync: %s", dl.Command) } if len(spec.Containers) != 1 { @@ -142,3 +143,29 @@ func TestBuildRestoreJobDownloadsWithMcThenRestores(t *testing.T) { t.Fatalf("restore job name %q is %d bytes; Kubernetes job-name labels must be <=63", job.Name, n) } } + +// A custom endpoint (MinIO) is addressed path-style through --endpoint-url; +// AWS itself by region, with neither. +func TestS3EnvAddressing(t *testing.T) { + env := func(spec hyperbytedbv1alpha1.S3BackupSpec) map[string]string { + out := map[string]string{} + for _, e := range s3Env(spec) { + out[e.Name] = e.Value + } + return out + } + minio := env(hyperbytedbv1alpha1.S3BackupSpec{Bucket: "b", Endpoint: "http://minio:9000"}) + if minio["S3_PATH_STYLE"] != "1" || minio["S3_ENDPOINT"] != "http://minio:9000" { + t.Errorf("custom endpoint env = %v", minio) + } + aws := env(hyperbytedbv1alpha1.S3BackupSpec{Bucket: "b", Region: "eu-west-2"}) + if _, ok := aws["S3_PATH_STYLE"]; ok { + t.Errorf("AWS must not use path-style: %v", aws) + } + if aws["AWS_DEFAULT_REGION"] != "eu-west-2" { + t.Errorf("region = %q", aws["AWS_DEFAULT_REGION"]) + } + if got := env(hyperbytedbv1alpha1.S3BackupSpec{Bucket: "b"})["AWS_DEFAULT_REGION"]; got != "us-east-1" { + t.Errorf("default region = %q", got) + } +} diff --git a/internal/controller/hyperbytedbbackup_controller.go b/internal/controller/hyperbytedbbackup_controller.go index 862ea0d..e341c20 100644 --- a/internal/controller/hyperbytedbbackup_controller.go +++ b/internal/controller/hyperbytedbbackup_controller.go @@ -220,7 +220,7 @@ func (r *HyperbytedbBackupReconciler) buildBackupJob(backup *hyperbytedbv1alpha1 Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyOnFailure, InitContainers: []corev1.Container{snapshotBackupInitContainer(hyperbytedb.ResolveHyperbytedbImage(cluster))}, - Containers: []corev1.Container{mcUploadContainer(s3, backup.Spec.RetentionDays)}, + Containers: []corev1.Container{s3UploadContainer(s3, backup.Spec.RetentionDays)}, Volumes: []corev1.Volume{ dataVolume(cluster.Name, 0, true), snapshotVolume(), diff --git a/internal/controller/hyperbytedbcluster_controller.go b/internal/controller/hyperbytedbcluster_controller.go index c528233..4e266c2 100644 --- a/internal/controller/hyperbytedbcluster_controller.go +++ b/internal/controller/hyperbytedbcluster_controller.go @@ -51,6 +51,9 @@ const clusterFinalizer = "hyperbytedb.hyperbyte.cloud/finalizer" // nodes are within tolerance of each other on parquet file count. const replicationStateHealthy = "Healthy" +// Condition set once the Raft cluster has formed. +const conditionClusterFormed = "ClusterFormed" + // HyperbytedbClusterReconciler reconciles a HyperbytedbCluster object. type HyperbytedbClusterReconciler struct { client.Client @@ -192,7 +195,7 @@ func (r *HyperbytedbClusterReconciler) Reconcile(ctx context.Context, req ctrl.R stsResult, err := r.reconcileStatefulSet(ctx, cluster, configHash) if err != nil { if apierrors.IsConflict(err) { - return ctrl.Result{Requeue: true}, nil + return ctrl.Result{RequeueAfter: time.Second}, nil } return r.setFailedStatus(ctx, cluster, "StatefulSetFailed", err) } @@ -1510,7 +1513,7 @@ func (r *HyperbytedbClusterReconciler) reconcileClusterMembership( if leaderHost == "" { log.V(1).Info("Could not locate raft leader from any pod; will retry next reconcile") meta.SetStatusCondition(&cluster.Status.Conditions, metav1.Condition{ - Type: "ClusterFormed", + Type: conditionClusterFormed, Status: metav1.ConditionFalse, Reason: "NoLeader", Message: "No raft leader reachable yet", @@ -1586,7 +1589,7 @@ func (r *HyperbytedbClusterReconciler) reconcileClusterMembership( if allMembers && len(knownIDs) >= int(replicas) { meta.SetStatusCondition(&cluster.Status.Conditions, metav1.Condition{ - Type: "ClusterFormed", + Type: conditionClusterFormed, Status: metav1.ConditionTrue, Reason: "AllNodesMembers", Message: fmt.Sprintf("All %d expected nodes are raft members", replicas), @@ -1594,7 +1597,7 @@ func (r *HyperbytedbClusterReconciler) reconcileClusterMembership( }) } else { meta.SetStatusCondition(&cluster.Status.Conditions, metav1.Condition{ - Type: "ClusterFormed", + Type: conditionClusterFormed, Status: metav1.ConditionFalse, Reason: "MembershipIncomplete", Message: fmt.Sprintf("%d/%d expected nodes are raft members", diff --git a/internal/controller/hyperbytedbrestore_controller.go b/internal/controller/hyperbytedbrestore_controller.go index 4576c29..673585d 100644 --- a/internal/controller/hyperbytedbrestore_controller.go +++ b/internal/controller/hyperbytedbrestore_controller.go @@ -256,7 +256,7 @@ func (r *HyperbytedbRestoreReconciler) buildRestoreJob( Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyOnFailure, - InitContainers: []corev1.Container{mcDownloadInitContainer(*s3Source)}, + InitContainers: []corev1.Container{s3DownloadInitContainer(*s3Source)}, Containers: []corev1.Container{restoreDataContainer(hyperbytedb.ResolveHyperbytedbImage(cluster), ordinal)}, Volumes: []corev1.Volume{ dataVolume(cluster.Name, ordinal, false), diff --git a/internal/controller/s3_transfer.go b/internal/controller/s3_transfer.go index b9900fb..04a211f 100644 --- a/internal/controller/s3_transfer.go +++ b/internal/controller/s3_transfer.go @@ -9,8 +9,13 @@ import ( hyperbytedbv1alpha1 "github.com/hyperbyte-cloud/hyperbytedb-operator/api/v1alpha1" ) -// Same tag Kind already pulls for the MinIO bucket job. The image includes /bin/sh. -const defaultS3ClientImage = "minio/mc:RELEASE.2024-11-21T17-21-54Z" +// The S3 client for backup upload and restore download. minio/mc is gone +// from Docker Hub (and quay.io/minio needs a login), so jobs on a node +// without it cached could not start. The AWS CLI image is maintained, has +// /bin/sh, works with MinIO and other S3-compatible stores, and trusts the +// CA bundle shipped in botocore, which no longer holds the compromised +// eMudhra emSign ECC Root CA - G3 (CVE-2026-111943). +const defaultS3ClientImage = "amazon/aws-cli:2.38.5" const ( snapshotVolumeName = "snapshot" @@ -19,6 +24,11 @@ const ( dataMountPath = "/var/lib/hyperbytedb" ) +// shellCommand runs script with /bin/sh, stopping at the first failure. +func shellCommand(script string) []string { + return []string{"/bin/sh", "-ec", script} +} + func s3ClientImage() string { return defaultS3ClientImage } @@ -41,11 +51,27 @@ func s3Prefix(s3 hyperbytedbv1alpha1.S3BackupSpec) string { return p + "/" } +func s3Region(s3 hyperbytedbv1alpha1.S3BackupSpec) string { + if s3.Region != "" { + return s3.Region + } + return "us-east-1" +} + func s3Env(s3 hyperbytedbv1alpha1.S3BackupSpec, extra ...corev1.EnvVar) []corev1.EnvVar { env := []corev1.EnvVar{ {Name: "S3_ENDPOINT", Value: s3Endpoint(s3)}, {Name: "S3_BUCKET", Value: s3.Bucket}, {Name: "S3_PREFIX", Value: s3Prefix(s3)}, + {Name: "AWS_DEFAULT_REGION", Value: s3Region(s3)}, + // No credential files or config in the image's home: everything + // comes from the environment. + {Name: "AWS_CONFIG_FILE", Value: "/tmp/aws-config"}, + } + // A custom endpoint (MinIO and other S3-compatible stores) takes + // path-style bucket addressing; AWS itself is addressed by region. + if s3.Endpoint != "" { + env = append(env, corev1.EnvVar{Name: "S3_PATH_STYLE", Value: "1"}) } if s3.CredentialsSecretName != "" { env = append(env, @@ -95,10 +121,10 @@ func snapshotBackupInitContainer(image string) corev1.Container { return corev1.Container{ Name: "snapshot", Image: image, - Command: []string{"/bin/sh", "-ec", `echo "Starting hyperbytedb backup..." + Command: shellCommand(`echo "Starting hyperbytedb backup..." hyperbytedb backup --output /snapshot echo "Backup size: $(du -sh /snapshot | cut -f1)" -`}, +`), VolumeMounts: []corev1.VolumeMount{ {Name: dataVolumeName, MountPath: dataMountPath, ReadOnly: true}, {Name: snapshotVolumeName, MountPath: snapshotMountPath}, @@ -106,23 +132,46 @@ echo "Backup size: $(du -sh /snapshot | cut -f1)" } } -func mcUploadContainer(s3 hyperbytedbv1alpha1.S3BackupSpec, retentionDays int32) corev1.Container { +// awsSetup is the preamble of every S3 client script: path-style addressing +// for a custom endpoint, and $EP for the endpoint flag. +const awsSetup = `if [ -n "${S3_PATH_STYLE:-}" ]; then + aws configure set default.s3.addressing_style path + EP="--endpoint-url ${S3_ENDPOINT}" +else + EP="" +fi +` + +func s3UploadContainer(s3 hyperbytedbv1alpha1.S3BackupSpec, retentionDays int32) corev1.Container { if retentionDays < 1 { retentionDays = 7 } - script := fmt.Sprintf(`mc alias set dest "${S3_ENDPOINT}" "${AWS_ACCESS_KEY_ID}" "${AWS_SECRET_ACCESS_KEY}" -TIMESTAMP=$(date +%%Y%%m%%d-%%H%%M%%S) -DEST="dest/${S3_BUCKET}/${S3_PREFIX}${TIMESTAMP}" + // Backups go to /; retention deletes the + // timestamped folders older than the cutoff, by name, as mc's + // --older-than did by object age. + script := awsSetup + fmt.Sprintf(`TIMESTAMP=$(date -u +%%Y%%m%%d-%%H%%M%%S) +DEST="s3://${S3_BUCKET}/${S3_PREFIX}${TIMESTAMP}/" echo "Uploading to ${DEST}..." -mc mirror /snapshot "${DEST}/" +aws $EP s3 sync /snapshot "${DEST}" --only-show-errors echo "Cleaning up backups older than %d days..." -mc rm --recursive --force --older-than "%dd" "dest/${S3_BUCKET}/${S3_PREFIX}" || true +CUTOFF=$(date -u -d "-%d days" +%%Y%%m%%d-%%H%%M%%S) +aws $EP s3 ls "s3://${S3_BUCKET}/${S3_PREFIX}" | awk '$1 == "PRE" { print $2 }' | while read -r dir; do + ts="${dir%%/}" + case "$ts" in + [0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) ;; + *) continue ;; + esac + if [ "$ts" \< "$CUTOFF" ]; then + echo "Removing ${dir}" + aws $EP s3 rm --recursive "s3://${S3_BUCKET}/${S3_PREFIX}${dir}" --only-show-errors + fi +done || true echo "Backup complete" `, retentionDays, retentionDays) return corev1.Container{ Name: "upload", Image: s3ClientImage(), - Command: []string{"/bin/sh", "-ec", script}, + Command: shellCommand(script), Env: s3Env(s3), VolumeMounts: []corev1.VolumeMount{ {Name: snapshotVolumeName, MountPath: snapshotMountPath, ReadOnly: true}, @@ -130,15 +179,24 @@ echo "Backup complete" } } -func mcDownloadInitContainer(s3 hyperbytedbv1alpha1.S3BackupSpec) corev1.Container { +// s3DownloadInitContainer fetches the newest timestamped backup under the +// prefix into /snapshot//, where the restore step looks for it; +// with none, the whole prefix (a backup stored at its root). +func s3DownloadInitContainer(s3 hyperbytedbv1alpha1.S3BackupSpec) corev1.Container { return corev1.Container{ Name: "download", Image: s3ClientImage(), - Command: []string{"/bin/sh", "-ec", `mc alias set dest "${S3_ENDPOINT}" "${AWS_ACCESS_KEY_ID}" "${AWS_SECRET_ACCESS_KEY}" -SRC="dest/${S3_BUCKET}/${S3_PREFIX}" -echo "Downloading backup from ${SRC}..." -mc mirror "${SRC}" /snapshot/ -`}, + Command: shellCommand(awsSetup + `SRC="s3://${S3_BUCKET}/${S3_PREFIX}" +LATEST=$(aws $EP s3 ls "${SRC}" | awk '$1 == "PRE" { print $2 }' \ + | grep -E '^[0-9]{8}-[0-9]{6}/$' | sort | tail -1 || true) +if [ -n "${LATEST}" ]; then + echo "Downloading backup ${SRC}${LATEST}..." + aws $EP s3 sync "${SRC}${LATEST}" "/snapshot/${LATEST}" --only-show-errors +else + echo "Downloading backup from ${SRC}..." + aws $EP s3 sync "${SRC}" /snapshot/ --only-show-errors +fi +`), Env: s3Env(s3), VolumeMounts: []corev1.VolumeMount{ {Name: snapshotVolumeName, MountPath: snapshotMountPath}, @@ -162,7 +220,7 @@ echo "Restore complete for ordinal %d" return corev1.Container{ Name: "restore", Image: image, - Command: []string{"/bin/sh", "-ec", script}, + Command: shellCommand(script), VolumeMounts: []corev1.VolumeMount{ {Name: dataVolumeName, MountPath: dataMountPath}, {Name: snapshotVolumeName, MountPath: snapshotMountPath, ReadOnly: true}, diff --git a/internal/hyperbytedb/compat.go b/internal/hyperbytedb/compat.go index 5df6670..1027325 100644 --- a/internal/hyperbytedb/compat.go +++ b/internal/hyperbytedb/compat.go @@ -63,16 +63,22 @@ type CompatibilityEntry struct { Without string } +// version090Beta introduced most of the capabilities below. +const version090Beta = "0.9.0-beta" + +// sourceAssumed is a DatabaseVersion's Source when no version was found. +const sourceAssumed = "assumed" + // CompatibilityMatrix lists what each capability needs. docs/compatibility.md // is generated from the same facts; keep them in step. var CompatibilityMatrix = []CompatibilityEntry{ - {CapabilitySplitChdbPools, "0.9.0-beta", "writes pool_size (spec.chdb.poolSize, else queryPoolSize, else 1) for both pools"}, - {CapabilityAutoQueryPool, "0.9.0-beta", "an unset query pool is written as pool_size = 1, as operator v0.1.x did"}, - {CapabilitySharding, "0.9.0-beta", "omits the [sharding] section; spec.sharding has no effect"}, - {CapabilityControlListener, "0.9.0-beta", "ignores the control-listener annotation (no control port, liveness on the server port)"}, - {CapabilityEvacuateTogether, "0.9.0-beta", "ignores the evacuate-together annotation (departing nodes leave one at a time)"}, + {CapabilitySplitChdbPools, version090Beta, "writes pool_size (spec.chdb.poolSize, else queryPoolSize, else 1) for both pools"}, + {CapabilityAutoQueryPool, version090Beta, "an unset query pool is written as pool_size = 1, as operator v0.1.x did"}, + {CapabilitySharding, version090Beta, "omits the [sharding] section; spec.sharding has no effect"}, + {CapabilityControlListener, version090Beta, "ignores the control-listener annotation (no control port, liveness on the server port)"}, + {CapabilityEvacuateTogether, version090Beta, "ignores the evacuate-together annotation (departing nodes leave one at a time)"}, {CapabilityClusterAuth, "0.9.1-beta", "renders auth.enabled only; no auth Secret, so a cluster with auth on cannot reach its internal routes"}, - {CapabilityProxyAdminListener, "0.9.0-beta", "probes the proxy and calls its admin API on its client port, where proxies before 0.9.0-beta serve them"}, + {CapabilityProxyAdminListener, version090Beta, "probes the proxy and calls its admin API on its client port, where proxies before 0.9.0-beta serve them"}, } // DatabaseVersion is the HyperbyteDB version a cluster is rendered for. @@ -128,7 +134,7 @@ func ResolveDatabaseVersion(cluster *v1alpha1.HyperbytedbCluster) DatabaseVersio } } v, _ := parseDatabaseVersion(LatestDatabaseVersion) - return DatabaseVersion{Version: v, Source: "assumed", Raw: LatestDatabaseVersion} + return DatabaseVersion{Version: v, Source: sourceAssumed, Raw: LatestDatabaseVersion} } // ResolveProxyVersion is the version of the proxy the cluster runs: the tag @@ -175,7 +181,7 @@ func CompatibilitySummary(cluster *v1alpha1.HyperbytedbCluster) (reason, message } reason = "Supported" switch { - case dv.Source == "assumed": + case dv.Source == sourceAssumed: reason = "Assumed" case dv.Version.Compare(semver.MustParse(OldestDatabaseVersion)) < 0: reason = "OlderThanSupported" @@ -183,7 +189,7 @@ func CompatibilitySummary(cluster *v1alpha1.HyperbytedbCluster) (reason, message reason = "NewerThanKnown" } message = fmt.Sprintf("HyperbyteDB %s (from %s)", dv, dv.Source) - if dv.Source == "assumed" { + if dv.Source == sourceAssumed { message += fmt.Sprintf("; set %s or a version image tag to pin it", DatabaseVersionAnnotation) } if len(missing) > 0 { diff --git a/internal/hyperbytedb/statefulset.go b/internal/hyperbytedb/statefulset.go index c4fc321..3364392 100644 --- a/internal/hyperbytedb/statefulset.go +++ b/internal/hyperbytedb/statefulset.go @@ -31,6 +31,12 @@ const ( annotationTrue = "true" ) +// Pod volumes the init containers fill for the database container. +const ( + sharedVolumeName = "shared" + entrypointVolumeName = "entrypoint" +) + func StatefulSetName(cluster *v1alpha1.HyperbytedbCluster) string { return cluster.Name } @@ -90,13 +96,13 @@ exec hyperbytedb --config /etc/hyperbytedb/config.toml serve }, }, { - Name: "shared", + Name: sharedVolumeName, VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }, { - Name: "entrypoint", + Name: entrypointVolumeName, VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{}, }, @@ -129,7 +135,7 @@ exec hyperbytedb --config /etc/hyperbytedb/config.toml serve Image: "busybox:1.36", Command: []string{"sh", "-c", initScript}, VolumeMounts: []corev1.VolumeMount{ - {Name: "shared", MountPath: "/shared"}, + {Name: sharedVolumeName, MountPath: "/shared"}, }, }, { @@ -137,7 +143,7 @@ exec hyperbytedb --config /etc/hyperbytedb/config.toml serve Image: "busybox:1.36", Command: []string{"sh", "-c", "printf '%s' '" + entrypoint + "' > /entrypoint/start.sh && chmod +x /entrypoint/start.sh"}, VolumeMounts: []corev1.VolumeMount{ - {Name: "entrypoint", MountPath: "/entrypoint"}, + {Name: entrypointVolumeName, MountPath: "/entrypoint"}, }, }, } @@ -174,8 +180,8 @@ exec hyperbytedb --config /etc/hyperbytedb/config.toml serve volumeMounts := []corev1.VolumeMount{ {Name: "data", MountPath: "/var/lib/hyperbytedb"}, {Name: "config", MountPath: "/etc/hyperbytedb", ReadOnly: true}, - {Name: "shared", MountPath: "/shared", ReadOnly: true}, - {Name: "entrypoint", MountPath: "/entrypoint", ReadOnly: true}, + {Name: sharedVolumeName, MountPath: "/shared", ReadOnly: true}, + {Name: entrypointVolumeName, MountPath: "/entrypoint", ReadOnly: true}, } if cluster.Spec.Server.TLS != nil && cluster.Spec.Server.TLS.Enabled {