Repository navigation
feat(api): add spec.extraArgs with a denylist of operator-managed flags #379
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1d3c81e
150e6b2
7e4dc2d
b99cb15
98532ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1166,6 +1166,72 @@ spec: | |
| rule: '!has(self.source.pvc) || (has(self.source.pvc.subPath) | ||
| && size(self.source.pvc.subPath) > 0)' | ||
| type: object | ||
| extraArgs: | ||
| additionalProperties: | ||
| type: string | ||
| description: |- | ||
| ExtraArgs passes additional flags to each member's etcd command line. | ||
| Keys are flag names without the leading dashes; each entry renders as | ||
| --key=value, with the value passed as is. Entries are appended after | ||
| the operator's own flags, sorted by key so the rendered Pod is | ||
| deterministic. | ||
|
|
||
| The operator owns a set of flags and rejects them here: member | ||
| identity and data placement (--name, --data-dir), the listen, | ||
| advertise and initial-cluster wiring, the TLS material it mounts from | ||
| spec.tls, alternative bootstrap mechanisms (--discovery*, | ||
| --config-file), and the flags typed by spec.options. The apiserver | ||
| rejects such a key on admission, naming the flag; the member | ||
| controller refuses to render one as a second guard. That denylist is | ||
| what made the original free-form map unsafe — it let a user break the | ||
| operator's own wiring — so with it in place a free-form map is safe | ||
| to offer. It is still needed next to spec.options: a typed set only | ||
| grows with the operator's releases, while a cluster may run an image | ||
| that wraps etcd and takes flags of its own, or need an etcd flag no | ||
| field covers yet. Prefer a typed field where one exists; this map is | ||
| not validated beyond the denylist, and a flag etcd does not know | ||
| makes the member fail to start. | ||
|
|
||
| Like spec.options, extraArgs are latched through status.observed and | ||
| take effect on newly-created members (scale-up, replacement) only; | ||
| the operator does not roll existing Pods to apply a change in place. | ||
| maxProperties: 64 | ||
| type: object | ||
| x-kubernetes-validations: | ||
| - messageExpression: '''spec.extraArgs keys are etcd flag names without | ||
| the leading dashes (for example log-level); invalid key: '' + | ||
| self.filter(k, !k.matches(''^[a-z0-9]+(-[a-z0-9]+)*$''))[0]' | ||
| rule: self.all(k, k.matches('^[a-z0-9]+(-[a-z0-9]+)*$')) | ||
| - messageExpression: '''spec.extraArgs: --'' + self.filter(k, k in | ||
| [''name'', ''data-dir'', ''wal-dir'', ''listen-peer-urls'', ''listen-client-urls'', | ||
| ''listen-client-http-urls'', ''listen-metrics-urls'', ''advertise-client-urls'', | ||
| ''initial-advertise-peer-urls'', ''initial-cluster'', ''initial-cluster-token'', | ||
| ''initial-cluster-state'', ''force-new-cluster'', ''discovery'', | ||
| ''discovery-srv'', ''discovery-endpoints'', ''config-file'', ''auto-tls'', | ||
| ''cert-file'', ''key-file'', ''client-cert-file'', ''client-key-file'', | ||
| ''client-cert-auth'', ''trusted-ca-file'', ''peer-auto-tls'', | ||
| ''peer-cert-file'', ''peer-key-file'', ''peer-client-cert-file'', | ||
| ''peer-client-key-file'', ''peer-trusted-ca-file'', ''peer-client-cert-auth''])[0] | ||
| + '' is managed by the operator and cannot be set here''' | ||
| rule: self.all(k, !(k in ['name', 'data-dir', 'wal-dir', 'listen-peer-urls', | ||
| 'listen-client-urls', 'listen-client-http-urls', 'listen-metrics-urls', | ||
| 'advertise-client-urls', 'initial-advertise-peer-urls', 'initial-cluster', | ||
| 'initial-cluster-token', 'initial-cluster-state', 'force-new-cluster', | ||
| 'discovery', 'discovery-srv', 'discovery-endpoints', 'config-file', | ||
| 'auto-tls', 'cert-file', 'key-file', 'client-cert-file', 'client-key-file', | ||
| 'client-cert-auth', 'trusted-ca-file', 'peer-auto-tls', 'peer-cert-file', | ||
| 'peer-key-file', 'peer-client-cert-file', 'peer-client-key-file', | ||
| 'peer-trusted-ca-file', 'peer-client-cert-auth'])) | ||
| - messageExpression: '''spec.extraArgs: --'' + self.filter(k, k in | ||
| [''quota-backend-bytes'', ''auto-compaction-mode'', ''auto-compaction-retention'', | ||
| ''snapshot-count''])[0] + '' is a typed option; set spec.options.'' | ||
| + {''quota-backend-bytes'': ''quotaBackendBytes'', ''auto-compaction-mode'': | ||
| ''autoCompactionMode'', ''auto-compaction-retention'': ''autoCompactionRetention'', | ||
| ''snapshot-count'': ''snapshotCount''}[self.filter(k, k in [''quota-backend-bytes'', | ||
| ''auto-compaction-mode'', ''auto-compaction-retention'', ''snapshot-count''])[0]] | ||
| + '' instead''' | ||
| rule: self.all(k, !(k in ['quota-backend-bytes', 'auto-compaction-mode', | ||
| 'auto-compaction-retention', 'snapshot-count'])) | ||
|
Comment on lines
+1169
to
+1234
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: sed -n '1168,1205p' charts/etcd-operator/crd-bases/etcd-operator.cozystack.io_etcdclusters.yaml
rg -n 'KUBEBUILDER_ASSETS|ENVTEST|setup-envtest|MaxProperties|cost budget' Makefile controllers/extra_args_envtest_test.go api/v1alpha2/etcdcluster_types.go | head -90Repository: cozystack/etcd-operator Length of output: 3738 🏁 Script executed: set -u
printf '%s\n' '== available envtest and Kubernetes binaries =='
find . -maxdepth 4 -type f \( -name 'setup-envtest' -o -name 'kube-apiserver' -o -name 'kubectl' -o -name 'kube-controller-manager' \) -print 2>/dev/null || true
find "${HOME}/.cache" "${HOME}/go" /tmp -type f \( -name 'kube-apiserver' -o -name 'setup-envtest' \) -print 2>/dev/null | head -80 || true
printf '%s\n' '== local Kubernetes CEL cost sources =='
find "${HOME}/go/pkg/mod" -type f \( -path '*/k8s.io/apiextensions-apiserver@*/pkg/apiserver/schema/cel/*' -o -path '*/k8s.io/apiextensions-apiserver@*/pkg/apiserver/schema/cel/*.go' -o -path '*/k8s.io/apiextensions-apiserver@*/pkg/apiserver/schema/cel/*cost*' \) -print 2>/dev/null | head -120 || true
rg -n --glob '*.go' 'EstimatedCost|estimated cost|cost budget|MaxProperties|MaxItems|RegexCost|costEstimator|HasCost' "${HOME}/go/pkg/mod/k8s.io" 2>/dev/null | head -160 || true
printf '%s\n' '== relevant module versions =='
rg -n 'k8s.io/(api|apiextensions-apiserver|apimachinery|client-go|controller-runtime)' go.mod go.sum | head -80Repository: cozystack/etcd-operator Length of output: 32780 🏁 Script executed: set -u
base="${HOME}/go/pkg/mod/k8s.io/apiextensions-apiserver@v0.33.0"
printf '%s\n' '== cel_validation.go cardinality and cost logic =='
nl -ba "$base/pkg/apis/apiextensions/validation/cel_validation.go" | sed -n '1,380p'
printf '%s\n' '== validation constants and per-CRD tests =='
nl -ba "$base/pkg/apis/apiextensions/validation/validation.go" | sed -n '45,75p'
nl -ba "$base/pkg/apis/apiextensions/validation/validation_test.go" | sed -n '10620,10720p'
nl -ba "$base/pkg/apis/apiextensions/validation/validation_test.go" | sed -n '11440,11580p'
printf '%s\n' '== cost-related implementation references =='
rg -n --glob '*.go' 'PerCRDEstimatedCost|StaticEstimatedCostLimit|MaxCardinality|EstimatedCost' "$base/pkg/apis/apiextensions/validation" "$base/pkg" | head -160Repository: cozystack/etcd-operator Length of output: 41877 🏁 Script executed: set -u
api="${HOME}/go/pkg/mod/k8s.io/apiextensions-apiserver@v0.33.0"
apiserver="${HOME}/go/pkg/mod/k8s.io/apiserver@v0.33.0"
printf '%s\n' '== CRD validation cost application =='
nl -ba "$api/pkg/apis/apiextensions/validation/validation.go" | sed -n '1180,1335p'
printf '%s\n' '== CEL compilation cost structures =='
nl -ba "$api/pkg/apiserver/schema/cel/compilation.go" | sed -n '1,310p'
printf '%s\n' '== Kubernetes CEL cost implementation references =='
rg -n --glob '*.go' 'EstimateCost|EstimatedCost|MaxCost|regex|matches|MaxCardinality|cost' "$apiserver/pkg/cel" "$apiserver/pkg" 2>/dev/null | head -240Repository: cozystack/etcd-operator Length of output: 25621 🏁 Script executed: set -u
api="${HOME}/go/pkg/mod/k8s.io/apiextensions-apiserver@v0.33.0"
apiserver="${HOME}/go/pkg/mod/k8s.io/apiserver@v0.33.0"
printf '%s\n' '== Kubernetes size estimator =='
nl -ba "$api/pkg/apiserver/schema/cel/compilation.go" | sed -n '300,460p'
printf '%s\n' '== Kubernetes CEL library cost estimator =='
find "$apiserver" "${HOME}/go/pkg/mod/github.com/google" -type f -name '*.go' -print 2>/dev/null | xargs -r rg -n -l 'type CostEstimator|SizeEstimator|EstimateCost|matches' | head -80
rg -n --glob '*.go' 'type CostEstimator|func .*EstimateCost|SizeEstimator|String.*Size|Regex|matches' "$apiserver/pkg/cel" "${HOME}/go/pkg/mod/github.com/google/cel-go@" 2>/dev/null | head -220
printf '%s\n' '== cel-go module versions =='
find "${HOME}/go/pkg/mod/github.com/google" -maxdepth 2 -type d -name 'cel-go@*' -print 2>/dev/nullRepository: cozystack/etcd-operator Length of output: 3325 🏁 Script executed: set -u
printf '%s\n' '== module-cache directories =='
find "${HOME}/go/pkg/mod/k8s.io" "${HOME}/go/pkg/mod/github.com" -maxdepth 2 -type d \( -name 'apiserver@*' -o -name 'cel-go@*' \) -print 2>/dev/null | head -100
printf '%s\n' '== exact estimator definitions =='
rg -n --hidden --glob '*.go' 'type CostEstimator|MaxElements|func .*EstimateCallCost|EstimateCost' "${HOME}/go/pkg/mod" 2>/dev/null | head -260
printf '%s\n' '== schema model MaxElements definitions =='
rg -n --hidden --glob '*.go' 'MaxElements|MinSerializedSize|MaxSerializedSize' "${HOME}/go/pkg/mod/k8s.io/apiextensions-apiserver@v0.33.0" "${HOME}/go/pkg/mod" 2>/dev/null | head -220Repository: cozystack/etcd-operator Length of output: 44326 🏁 Script executed: set -u
model="${HOME}/go/pkg/mod/k8s.io/apiextensions-apiserver@v0.33.0/pkg/apiserver/schema/cel/model"
printf '%s\n' '== schema model implementation and string cases =='
rg -n --glob '*.go' 'MaxElements|DefaultMaxRequestSizeBytes|MaxLength|Type == "string"|case "string"' "$model" | head -180
nl -ba "$model/schemas.go" | sed -n '1,280p'
nl -ba "$model/schemas_test.go" | sed -n '250,490p'
printf '%s\n' '== module download cache =='
find "${HOME}/go/pkg/mod/cache/download" -type f \( -path '*/k8s.io/apiserver/*' -o -path '*/github.com/google/cel-go/*' \) -print 2>/dev/null | head -120
printf '%s\n' '== any cached estimator source outside module directories =='
rg -n --hidden --glob '*.go' 'type CostEstimator|EstimateCallCost.*matches|regex.*cost|CostEstimator struct' /home /tmp 2>/dev/null | head -120Repository: cozystack/etcd-operator Length of output: 17245 🌐 Web query:
💡 Result: 🌐 Web query:
💡 Result: 🌐 Web query:
💡 Result: Make
Change 🤖 Prompt for AI Agents |
||
| imagePullSecrets: | ||
| description: |- | ||
| ImagePullSecrets is a list of Secret references in the cluster's | ||
|
|
@@ -1197,8 +1263,7 @@ spec: | |
| description: |- | ||
| Options carries etcd server tuning flags (backend quota, | ||
| auto-compaction, raft snapshot count) passed to each member's | ||
| command line. A closed typed set — see EtcdOptions for why there | ||
| is deliberately no free-form flag map. | ||
| command line, typed and validated. See EtcdOptions. | ||
|
|
||
| Updates take effect on newly-created members (scale-up, | ||
| replacement); the operator does not roll existing Pods to apply a | ||
|
|
@@ -2883,6 +2948,15 @@ spec: | |
| x-kubernetes-list-type: atomic | ||
| type: object | ||
| type: object | ||
| extraArgs: | ||
| additionalProperties: | ||
| type: string | ||
| description: |- | ||
| ExtraArgs is the locked target set of additional etcd flags for | ||
| member Pods. Latched with the rest of the target spec so a mid-flight | ||
| edit only applies to members created once the current target is | ||
| reached. | ||
| type: object | ||
| imagePullSecrets: | ||
| description: |- | ||
| ImagePullSecrets is the locked target pull-secret list for member | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix the
spec.extraArgslifecycle text and the denylist sentence.Lines 588-590 say that
extraArgstake effect "on newly-created members (scale-up, replacement) only". This PR also synchronizes the latched value onto existing and dormantEtcdMemberspecs. A recreated Pod of an existing member therefore receives the new flags.EtcdMemberSpec.ExtraArgsinapi/v1alpha2/etcdmember_types.go:125-130describes this behavior correctly. The cluster-level text conflicts with it, and the generated CRD shows the cluster-level text tokubectl explainusers.Lines 579-581 say "That denylist is what made the original free-form map unsafe". The original map was unsafe because it had no denylist. As written, the sentence states the opposite.
Fix both points in the Go marker comment. Then regenerate
charts/etcd-operator/crd-bases/etcd-operator.cozystack.io_etcdclusters.yaml.📝 Proposed doc fix
📝 Committable suggestion
🤖 Prompt for AI Agents