You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by vi...@apache.org on 2026/06/05 23:25:52 UTC

(superset-kubernetes-operator) branch main updated: chore: image attestations, downgrade warning, release hygiene (#120)

This is an automated email from the ASF dual-hosted git repository.

villebro pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/superset-kubernetes-operator.git


The following commit(s) were added to refs/heads/main by this push:
     new 49ca48b  chore: image attestations, downgrade warning, release hygiene (#120)
49ca48b is described below

commit 49ca48b2d3863e0e9cd57ba7d321e2a6283447c0
Author: Ville Brofeldt <33...@users.noreply.github.com>
AuthorDate: Fri Jun 5 16:25:46 2026 -0700

    chore: image attestations, downgrade warning, release hygiene (#120)
---
 .gitattributes                                 |  6 ++++++
 .github/workflows/release.yml                  |  2 ++
 .rat-excludes                                  |  4 ++++
 docs/reference/releases.md                     | 14 ++++++++++++++
 docs/reference/security.md                     |  7 +++++--
 docs/user-guide/lifecycle.md                   |  7 +++++++
 internal/controller/lifecycle.go               | 13 +++++++++++++
 internal/controller/lifecycle_pipeline_test.go | 21 +++++++++++++++++++++
 8 files changed, 72 insertions(+), 2 deletions(-)

diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..6c993ab
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,6 @@
+# Keep editor/agent context files in-tree for local development, but exclude
+# them from the `git archive` source release tarball. AGENTS.md is the canonical
+# (vendor-neutral) file and ships in the release; CLAUDE.md and GEMINI.md are
+# symlinks to it and are tool-specific clutter in an official ASF source release.
+CLAUDE.md export-ignore
+GEMINI.md export-ignore
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index eafb96b..1d5bf63 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -72,6 +72,8 @@ jobs:
           push: true
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}
+          sbom: true
+          provenance: mode=max
           cache-from: type=gha
           cache-to: type=gha,mode=max
 
diff --git a/.rat-excludes b/.rat-excludes
index dc51770..e2e78e2 100644
--- a/.rat-excludes
+++ b/.rat-excludes
@@ -19,6 +19,7 @@
 
 # Git
 .*\.gitignore
+.*\.gitattributes
 
 # Go
 .*go\.mod
@@ -45,6 +46,9 @@
 .*PULL_REQUEST_TEMPLATE\.md
 .*ISSUE_TEMPLATE/.*\.md
 .*\.claude.*
+# Editor/agent context symlinks → AGENTS.md (which carries the header itself)
+CLAUDE\.md
+GEMINI\.md
 .*rat-results\.txt
 .*Dockerfile\.cross
 .*codecov\.yml
diff --git a/docs/reference/releases.md b/docs/reference/releases.md
index f101c72..5194d0c 100644
--- a/docs/reference/releases.md
+++ b/docs/reference/releases.md
@@ -30,5 +30,19 @@ releases.
 
 - Initial release.
 
+### Known limitations
+
+- **Websocket server is experimental.** The websocket server is not yet well
+  supported and is pending security hardening; it is not recommended for
+  production use.
+- **Downgrade protection requires semver image tags.** Downgrades are detected
+  and blocked only when both image tags are valid semver. Non-semver tags
+  (`latest`, date stamps, digest pins) cannot be ordered, so the operator emits a
+  `VersionComparisonSkipped` warning and proceeds without blocking. See
+  [Lifecycle](../user-guide/lifecycle.md).
+- **Task failure messages may include credential fragments.** Lifecycle task
+  failure output is truncated into `status` and could contain fragments of task
+  stdout, including credentials. See [security.md](security.md).
+
 [Unreleased]: https://github.com/apache/superset-kubernetes-operator/compare/v0.1.0...HEAD
 [0.1.0]: https://github.com/apache/superset-kubernetes-operator/releases/tag/v0.1.0
diff --git a/docs/reference/security.md b/docs/reference/security.md
index f7b8ec7..fd7979b 100644
--- a/docs/reference/security.md
+++ b/docs/reference/security.md
@@ -414,8 +414,11 @@ The release pipeline produces signed multi-architecture artifacts:
   so its digest refreshes are inherently un-soaked. This residual exposure is accepted
   and bounded by the minimal distroless base and by keyless Cosign signing of the
   image this project itself publishes.
-- **Future work:** SBOM and SLSA build provenance generation are tracked as
-  enhancements for later releases.
+- **SBOM & provenance:** Each published image carries a per-platform Software
+  Bill of Materials (SPDX) and SLSA build provenance, attached as in-toto
+  attestations by BuildKit at build time. Inspect them with
+  `docker buildx imagetools inspect ghcr.io/apache/superset-kubernetes-operator:<tag> --format '{{ json .SBOM }}'`
+  (or `.Provenance`), or with `cosign download attestation`.
 
 ## What Is Generally Out of Scope
 
diff --git a/docs/user-guide/lifecycle.md b/docs/user-guide/lifecycle.md
index 341665a..c4c02b7 100644
--- a/docs/user-guide/lifecycle.md
+++ b/docs/user-guide/lifecycle.md
@@ -178,6 +178,13 @@ The operator also performs semver comparison on image tags and blocks downgrades
 to prevent accidental database corruption. A blocked downgrade sets
 `status.phase: Blocked` — revert the image tag to resolve.
 
+Downgrade protection requires semantically comparable (semver) image tags.
+Non-semver tags — such as `latest`, date stamps, or digest pins — cannot be
+ordered, so the operator cannot detect a downgrade and lifecycle tasks proceed
+normally. In that case it emits a `VersionComparisonSkipped` warning event
+rather than blocking. Use semver tags in Staging and Production if you rely on
+downgrade protection.
+
 ## Drain Behavior
 
 Each task declares whether it requires components to be drained (scaled to zero)
diff --git a/internal/controller/lifecycle.go b/internal/controller/lifecycle.go
index ee2da2d..7978613 100644
--- a/internal/controller/lifecycle.go
+++ b/internal/controller/lifecycle.go
@@ -449,6 +449,19 @@ func (r *SupersetReconciler) checkUpgradeGates(
 	}
 
 	contextMatches := upgradeContextMatches(superset.Status.Lifecycle.Upgrade, oldTag, newTag, direction, approvalToken)
+
+	// Non-semver tags (e.g. "latest", date stamps, digest pins) cannot be
+	// ordered, so downgrade protection does not apply — a downgrade expressed
+	// with such tags would run forward-only migrations against an older image
+	// undetected. Surface this once per distinct image transition so operators
+	// can intervene; the !contextMatches guard prevents per-reconcile spam.
+	if direction == DirectionUnknown && !contextMatches {
+		log.Info("Image version change could not be compared (non-semver tags); downgrade protection skipped",
+			"from", oldTag, "to", newTag)
+		r.Recorder.Eventf(superset, nil, corev1.EventTypeWarning, "VersionComparisonSkipped", "Lifecycle",
+			"Cannot compare image tags %q and %q (non-semver); downgrade protection does not apply", oldTag, newTag)
+	}
+
 	if !contextMatches {
 		superset.Status.Lifecycle.Upgrade = &supersetv1alpha1.UpgradeContext{
 			FromVersion:   oldTag,
diff --git a/internal/controller/lifecycle_pipeline_test.go b/internal/controller/lifecycle_pipeline_test.go
index 438fa44..472de04 100644
--- a/internal/controller/lifecycle_pipeline_test.go
+++ b/internal/controller/lifecycle_pipeline_test.go
@@ -349,4 +349,25 @@ func TestCheckUpgradeGates(t *testing.T) {
 		assert.Equal(t, "2.0.0", s.Status.Lifecycle.Upgrade.FromVersion)
 		assert.Equal(t, "3.0.0", s.Status.Lifecycle.Upgrade.ToVersion)
 	})
+
+	t.Run("non-semver tags proceed but emit a warning", func(t *testing.T) {
+		rec := events.NewFakeRecorder(10)
+		nr := &SupersetReconciler{Recorder: rec}
+		s := &supersetv1alpha1.Superset{
+			ObjectMeta: metav1.ObjectMeta{Name: "test"},
+			Status:     supersetv1alpha1.SupersetStatus{Lifecycle: &supersetv1alpha1.LifecycleStatus{}},
+		}
+		// "latest" → "main" cannot be ordered: not a downgrade, so not gated.
+		_, gated := nr.checkUpgradeGates(ctx, s, true, "apache/superset:latest", "apache/superset:main")
+		assert.False(t, gated)
+		assert.NotEqual(t, lifecyclePhaseBlocked, s.Status.Lifecycle.Phase)
+		require.NotNil(t, s.Status.Lifecycle.Upgrade)
+		assert.Equal(t, string(DirectionUnknown), s.Status.Lifecycle.Upgrade.Direction)
+		select {
+		case ev := <-rec.Events:
+			assert.Contains(t, ev, "VersionComparisonSkipped")
+		default:
+			t.Fatal("expected a VersionComparisonSkipped warning event")
+		}
+	})
 }