Skip to content

Commit 4e42707

Browse files
pedjakclaude
andcommitted
🌱 Ensure COS phase immutability for referenced object approach
ClusterObjectSet phases are immutable by design, but when objects are stored in external Secrets via refs, the Secret content could be changed by deleting and recreating the Secret. This enforces phase immutability by: - Verifying that referenced Secrets have `immutable: true` set - Computing a per-phase SHA-256 content digest of pre-mutation resolved objects and recording it in `.status.observedPhases` - Blocking reconciliation (`Progressing=False, Reason=Blocked`) if any referenced Secret is mutable or any phase's digest has changed - Allowing blocked COS to recover when original content is restored The digest is source-agnostic — it covers fully resolved phase content regardless of whether objects are inline or from Secrets, making it forward-compatible with future object sources. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent dd57c28 commit 4e42707

26 files changed

Lines changed: 1092 additions & 224 deletions

.bingo/Variables.mk

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ $(CRD_REF_DOCS): $(BINGO_DIR)/crd-ref-docs.mod
4747
@echo "(re)installing $(GOBIN)/crd-ref-docs-v0.3.0"
4848
@cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=crd-ref-docs.mod -o=$(GOBIN)/crd-ref-docs-v0.3.0 "github.com/elastic/crd-ref-docs"
4949

50+
GOJQ := $(GOBIN)/gojq-v0.12.17
51+
$(GOJQ): $(BINGO_DIR)/gojq.mod
52+
@# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies.
53+
@echo "(re)installing $(GOBIN)/gojq-v0.12.17"
54+
@cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=gojq.mod -o=$(GOBIN)/gojq-v0.12.17 "github.com/itchyny/gojq/cmd/gojq"
55+
5056
GOLANGCI_LINT := $(GOBIN)/golangci-lint-v2.8.0
5157
$(GOLANGCI_LINT): $(BINGO_DIR)/golangci-lint.mod
5258
@# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies.

.bingo/gojq.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT
2+
3+
go 1.24.4
4+
5+
require github.com/itchyny/gojq v0.12.17 // cmd/gojq

.bingo/gojq.sum

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg=
2+
github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY=
3+
github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q=
4+
github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
5+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
6+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
7+
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
8+
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
9+
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
10+
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
11+
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
12+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13+
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
14+
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
15+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
16+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
17+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

.bingo/variables.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ CRD_DIFF="${GOBIN}/crd-diff-v0.5.1-0.20260309184313-54162f2e3097"
1818

1919
CRD_REF_DOCS="${GOBIN}/crd-ref-docs-v0.3.0"
2020

21+
GOJQ="${GOBIN}/gojq-v0.12.17"
22+
2123
GOLANGCI_LINT="${GOBIN}/golangci-lint-v2.8.0"
2224

2325
GORELEASER="${GOBIN}/goreleaser-v2.11.2"

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,8 @@ fmt: $(YAMLFMT) #EXHELP Formats code
221221
$(YAMLFMT) -gitignore_excludes testdata
222222

223223
.PHONY: update-tls-profiles
224-
update-tls-profiles: #EXHELP Update TLS profiles from the Mozilla wiki
225-
hack/tools/update-tls-profiles.sh
224+
update-tls-profiles: $(GOJQ) #EXHELP Update TLS profiles from the Mozilla wiki
225+
env JQ=$(GOJQ) hack/tools/update-tls-profiles.sh
226226

227227
.PHONY: update-registryv1-bundle-schema
228228
update-registryv1-bundle-schema: #EXHELP Update registry+v1 bundle configuration JSON schema

api/v1/clusterobjectset_types.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,39 @@ type ClusterObjectSetStatus struct {
510510
// +listMapKey=type
511511
// +optional
512512
Conditions []metav1.Condition `json:"conditions,omitempty"`
513+
514+
// observedPhases records the content hashes of resolved phases
515+
// at first successful reconciliation. This is used to detect if
516+
// referenced object sources were deleted and recreated with
517+
// different content. Each entry covers all fully-resolved object
518+
// manifests within a phase, making it source-agnostic.
519+
//
520+
// +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf.size() == 0",message="observedPhases is immutable"
521+
// +kubebuilder:validation:MaxItems=20
522+
// +listType=map
523+
// +listMapKey=name
524+
// +optional
525+
ObservedPhases []ObservedPhase `json:"observedPhases,omitempty"`
526+
}
527+
528+
// ObservedPhase records the observed content digest of a resolved phase.
529+
type ObservedPhase struct {
530+
// name is the phase name matching a phase in spec.phases.
531+
//
532+
// +required
533+
// +kubebuilder:validation:MinLength=1
534+
// +kubebuilder:validation:MaxLength=63
535+
// +kubebuilder:validation:XValidation:rule=`!format.dns1123Label().validate(self).hasValue()`,message="the value must consist of only lowercase alphanumeric characters and hyphens, and must start with an alphabetic character and end with an alphanumeric character."
536+
Name string `json:"name"`
537+
538+
// digest is the digest of the phase's resolved object content
539+
// at first successful resolution, in the format "<algorithm>:<hex>".
540+
//
541+
// +required
542+
// +kubebuilder:validation:MinLength=1
543+
// +kubebuilder:validation:MaxLength=256
544+
// +kubebuilder:validation:XValidation:rule=`self.matches('^[a-z0-9]+:[a-f0-9]+$')`,message="digest must be in the format '<algorithm>:<hex>'"
545+
Digest string `json:"digest"`
513546
}
514547

515548
// +genclient

api/v1/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

applyconfigurations/api/v1/clusterobjectsetstatus.go

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

applyconfigurations/api/v1/observedphase.go

Lines changed: 52 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

applyconfigurations/utils.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)