Skip to content

fix: support extraVolumes/extraVolumeMounts, fix WH CA handling (OP-388) - #2800

Open
kristina-solovyova wants to merge 1 commit into
mainfrom
09-07-fix_support_extravolumes_extravolumemounts_fix_wh_ca_handling_op-388_
Open

fix: support extraVolumes/extraVolumeMounts, fix WH CA handling (OP-388)#2800
kristina-solovyova wants to merge 1 commit into
mainfrom
09-07-fix_support_extravolumes_extravolumemounts_fix_wh_ca_handling_op-388_

Conversation

@kristina-solovyova

Copy link
Copy Markdown
Collaborator

No description provided.

kristina-solovyova commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more


How to use the Graphite Merge Queue

Add the label main-merge-queue to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has required the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@kristina-solovyova
kristina-solovyova changed the base branch from main to graphite-base/2800 September 8, 2026 07:14
@kristina-solovyova
kristina-solovyova force-pushed the 09-07-fix_support_extravolumes_extravolumemounts_fix_wh_ca_handling_op-388_ branch from b28f878 to 5378268 Compare September 8, 2026 07:14
@kristina-solovyova
kristina-solovyova changed the base branch from graphite-base/2800 to 09-03-fix_tlc_drive_core_capping_and_address_linting_issues September 8, 2026 07:14
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

1 similar comment
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@kristina-solovyova
kristina-solovyova force-pushed the 09-07-fix_support_extravolumes_extravolumemounts_fix_wh_ca_handling_op-388_ branch from 5378268 to 651ba76 Compare September 9, 2026 11:39
@kristina-solovyova
kristina-solovyova changed the base branch from graphite-base/2800 to main September 9, 2026 11:39
@kristina-solovyova
kristina-solovyova marked this pull request as ready for review September 9, 2026 11:39
Copilot AI lite review requested due to automatic review settings September 9, 2026 11:39
@kristina-solovyova
kristina-solovyova requested a review from a team as a code owner September 9, 2026 11:39
@graphite-app
graphite-app Bot requested a review from tigrawap September 9, 2026 11:40
@graphite-app

graphite-app Bot commented Sep 9, 2026

Copy link
Copy Markdown

Graphite Automations

"Add anton/matt/sergey/kristina as reviwers on operator PRs" took an action on this PR • (09/09/26)

2 reviewers were added to this PR based on Anton Bykov's automation.

@graphite-app
graphite-app Bot requested a review from assafgi September 9, 2026 11:40
@claude

claude Bot commented Sep 9, 2026

Copy link
Copy Markdown

Claude finished @kristina-solovyova's task in 8m 28s —— View job


Review summary

Reviewed all 42 files. This is careful, well-commented work — the digest-based comparison (working around HashStruct's gob encoding choking on CSI volumeAttributes and ignoring Quantity's unexported fields), the omitempty on the new pod-config-version inputs so operator upgrades don't force a roll, and the defence-in-depth between admission and the pod-factory backstop are all the right calls. I verified the ReservedVolumeNames list against every volume the operator actually creates in pod.go/drivers.go/init_containers.go — it's complete.

Two findings I'd consider blocking, plus a set of smaller ones. All posted as inline comments.

Blocking

1. Changing wekaHome.cacertSecret updates the container spec but never rolls the podpod_config_version.go

updateContainerIfChanged now propagates the resolved secret to container.Spec.AdditionalSecrets, which becomes a pod volume + mount (pod.go:815-828). But pods only recreate on container.Spec.PodConfigHash drift, and the resolved secret isn't an input to CalcClientPodConfigVersion. So the change reaches the container spec and stops there — the running pod keeps the old bundle and weka_cloud_ca_cert_path still points at stale/absent content. The comment at client_reconciler_loop.go:849-850 states the opposite intent.

2. Adoption branch can permanently swallow a pending image upgradeclient_reconciler_loop.go:1091

HandleUpgrade moved from image-tracked (TargetPodConfigHash == ""isContainerAligned compares images) to hash-tracked. For a client with Status.LastAppliedPodConfigHash == "" whose spec.image was bumped in the same apply as the operator upgrade, adoption stamps hash(newImage) without rolling, and the next reconcile short-circuits at line 1081. Containers stay on the old image forever, with no Upgrading state and no event. The wekacluster counterpart at least computes imageChanged first.

Correctness / security

  • /usr/bin/weka missing from ReservedMountPathsextra_volumes.go:44. init_containers.go:171 mounts the staged CLI there; /usr/local/bin/weka is reserved but /usr/bin/weka is not, so a user mount can shadow it. Includes a suggested one-line fix and a proposal for a test that would prevent the next drift (three entries already reserve names nothing creates anymore).
  • weka_runtime.py will inline a private key from a kubernetes.io/tls secretweka_runtime.py:3192. Concatenating every file means tls.key lands inside cert.pem; the BEGIN CERTIFICATE check still passes thanks to tls.crt. Compounded by chmod 400 running only after the loop, so the file exists at default umask during the write. Suggested a per-file cert filter that also makes the post-hoc content test unnecessary.
  • ExtraVolumeMountsDigest returns "" on marshal errorextra_volumes.go:119, which is indistinguishable from "unset" and makes callers skip propagation. Contradicts CLAUDE.md's "never swallow errors".
  • AdditionalSecrets map wholesale replacementclient_reconciler_loop.go:849. Latent today (one key), but the reserved-names list already depends on that same assumption in a second place. Also suggested a const for the thrice-repeated "wekahome-cacert" literal.
  • Cross-namespace warning event never reaches existing clientswekahome.go:70. crossNamespaceSkipped is only consumed at container-create; NewUpdatableClientSpec drops it with _. Plus a redundant re-check to simplify.
  • Helm /tmp guard is exact-match onlymanager.yaml:66; /tmp/foo slips through.
  • Minor validator inconsistenciesextra_volumes.go:77: seenPaths keyed on raw vs. path.Clean in the pod factory (accidentally equivalent, not obviously so); raw.Object silently ignored; whole raw JSON blob embedded in the admission error message.

Docs / API

  • doc/operator/deployment/extra-volumes.md documents symbols that don't existdetails: ReservedMountPathPrefixes and a prefixes/exact-paths split (there's one ReservedMountPaths slice with one rule), and "name suffixes" contradicts the file's own later explanation.
  • weka-home-tls.md mischaracterises the Helm valuedetails. "No volume, no mount, no container filesystem is involved" is wrong: env.Config.WekaHome.CacertSecret is the last-resort fallback in GetWekaHomeClientCacertSecret (this PR) and flows to backend pods via GetWekahomeConfigcontainer_factory.go:96. The client precedence chain also omits that third step.
  • wekaHomeConfig removed from v1alpha1details. Rides in via the pkg/weka-k8s-api submodule bump. It was a documented no-op, but SSA and --validate=strict will now hard-reject manifests that still set it. Worth an explicit release-note callout rather than only appearing as a deleted line in a generated doc.

Test coverage

Details on the test file. TestNewUpdatableClientSpec_HashStructSurvivesCsiVolumeAttributesMap is a genuinely valuable regression test. Gaps: no case for image-changed-during-adoption (finding 2) or for AllowRotateNonAnnotatedPodConfigHash = true; the c.upgradeInProgress assertion at line 368 is tautological (nothing on that path sets it — asserting container.Spec.PodConfigHash == "" would be the load-bearing check); wekahome_test.go:70-79 uses the same value for client and cluster so it passes under either precedence; and the warn-vs-error severity for the new cacert validator is untested.


Not done: I could not run go build, go test, or gofmt — those commands were denied in this environment, so nothing here is verified by execution. Every finding is from reading the code, and I traced each one to the specific call sites cited. Findings 1 and 2 in particular are worth confirming with a test before acting on them.
• branch 09-07-fix_support_extravolumes_extravolumemounts_fix_wh_ca_handling_op-388_

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It modifies core reconciliation/upgrade rollout mechanics (pod-config hashing + propagation) across cluster/client paths, so a final human review is warranted despite only minor documentation nits found.

Pull request overview

Adds end-to-end support for user-specified extraVolumes / extraVolumeMounts across WekaCluster and WekaClient (validation, propagation to WekaContainer, pod construction, and rollout via pod-config hashing), and improves Weka Home CA handling for WekaClient (inheritance from target cluster, cross-namespace warning, and an admission warning for unverifiable “client-only” topology).

Changes:

  • Implement extraVolumes / extraVolumeMounts validation (strict JSON decoding + reserved name/path protection) for both WekaCluster and WekaClient.
  • Propagate extra volumes/mounts into WekaContainer specs and into created Pods, with digest-based comparison to avoid churn and to support CSI/map-containing volumes safely.
  • Fix/extend Weka Home CA secret resolution for WekaClient (client→cluster(same-namespace)→env default), plus warnings/events and docs/Helm support.
File summaries
File Description
internal/validation/registry.go Registers new validators for extra volumes and Weka Home CA warning.
internal/validation/extra_volumes.go Shared extraVolumes/extraVolumeMounts validation logic (strict decode, reserved checks).
internal/validation/extra_volumes_test.go Table-driven tests covering validation rules and field paths.
internal/validation/cluster_extra_volumes.go WekaCluster wrapper validator for spec.podConfig.* extra volumes/mounts.
internal/validation/client_wekahome_cacert_unverifiable.go Admission warning for unverifiable client-only Weka Home CA override.
internal/validation/client_wekahome_cacert_unverifiable_test.go Tests for the client-only CA unverifiable warning behavior.
internal/validation/client_extra_volumes.go WekaClient wrapper validator for flat spec.* extra volumes/mounts.
internal/pkg/domain/wekahome.go Centralizes WekaClient Weka Home CA secret resolution with namespace rules.
internal/pkg/domain/wekahome_test.go Unit tests for CA secret precedence and cross-namespace behavior.
internal/controllers/wekacluster/pod_config_version.go Includes extra volume/mount digests in cluster pod-config hash (omitempty).
internal/controllers/wekacluster/pod_config_version_test.go Tests for hash changes with extras + unchanged hash when extras unset.
internal/controllers/wekacluster/funcs_upgrade.go Adds digest-based propagation of extras into containers during spec updates.
internal/controllers/wekacluster/funcs_upgrade_test.go Regression tests for CSI map safety + propagation/no-churn behavior.
internal/controllers/wekaclient/pod_config_version.go New client pod-config hash function including extra volume/mount digests.
internal/controllers/wekaclient/pod_config_version_test.go Tests for client hash stability/change and “unset extras” compatibility.
internal/controllers/wekaclient/client_reconciler_loop.go Propagates extras, resolves/inherits Weka Home CA secret, adds adoption logic for new pod-config tracking.
internal/controllers/wekaclient/client_reconciler_loop_test.go Tests for extras propagation, CSI map hashing safety, adoption behavior, and upgrade/image regression coverage.
internal/controllers/wekaclient/client_numa_test.go Updates tests for new NewUpdatableClientSpec(..., targetCluster) signature.
internal/controllers/resources/pod.go Applies extra volumes/mounts late in pod build with collision checks.
internal/controllers/resources/extra_volumes.go Defines reserved names/paths + normalization + digest helpers for extras.
internal/controllers/resources/extra_volumes_test.go PodFactory-level tests for collisions, normalization guarantees, and reserved drift guard.
internal/controllers/factory/container_factory.go Propagates normalized cluster extra volumes/mounts into new WekaContainers.
internal/admission/defaults.go Adds default severities for new validators (extras = Error; unverifiable CA = Warn).
doc/summary.xml Adds doc index entries for new extra-volumes and Weka Home TLS docs.
doc/operator/operations/weka-home-tls.md New operational doc explaining Weka Home TLS + CA distribution constraints and topologies.
doc/operator/operations/secrets-management.md Links to Weka Home TLS doc for CA-secret coverage clarification.
doc/operator/deployment/helm-install.md Documents Helm manager.extraVolumes / manager.extraVolumeMounts.
doc/operator/deployment/extra-volumes.md New doc describing all three extra-volumes surfaces, validation, rollout, and examples.
doc/api_dump/wekacontainer.md API dump updated with extraVolumes fields on WekaContainer.
doc/api_dump/wekacluster.md API dump updated with extraVolumes fields on WekaCluster podConfig.
doc/api_dump/wekaclient.md API dump updated with extraVolumes fields + client status pod-config hash.
doc/api_dump/api-schema.json Generated schema updates for new fields and removed deprecated field entry.
charts/weka-operator/values.yaml Adds Helm values for manager extra volumes/mounts.
charts/weka-operator/templates/manager.yaml Templates manager pod extra volumes/mounts + guards reserved name/path.
charts/weka-operator/resources/weka_runtime.py Improves WH CA secret handling by concatenating all PEM keys and validating content.
.ainav/index.md Adds navigation entry for validation/admission docs.
.ainav/controllers/wekacluster.md Adds Weka Home CA scope notes and pointers to the new doc.
.ainav/controllers/wekaclient.md Adds WekaClient Weka Home CA resolution behavior notes.
.ainav/config/validation.md New navigation doc for validation/admission structure and new rules.
.ainav/config/index.md Notes new manager extras and extraVolumes API surfaces with doc pointer.
Review details
  • Files reviewed: 42/42 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +89 to +90
**Reserved mount-path prefixes** (`ReservedMountPathPrefixes`) — a mount
cannot land on or under:
Comment thread doc/summary.xml
Comment on lines +56 to +59
<file relpath="operator/deployment/extra-volumes.md">
<tags>extraVolumes, extraVolumeMounts, podConfig, manager.extraVolumes, RawExtension, schemaless, VolumeMount, cluster_extra_volumes, client_extra_volumes, ReservedVolumeNames, ReservedMountPathPrefixes, DisallowUnknownFields, pod-config-version, podConfigHash, allowRotateNonAnnotatedPodConfigHash, upgradePolicy, CA bundle, tmpdir</tags>
<summary>Documents the three extra-volumes surfaces — WekaCluster spec.podConfig.extraVolumes/extraVolumeMounts (all pods of the cluster), WekaClient spec.extraVolumes/extraVolumeMounts (all client pods, flat spec, no podConfig), and the operator's own Helm manager.extraVolumes/extraVolumeMounts — for mounting arbitrary volumes (e.g. a private CA bundle) without a custom image. Explains that extraVolumes is a schemaless RawExtension while extraVolumeMounts is a typed VolumeMount list, and the resulting gap that only admission's DisallowUnknownFields decoding (cluster_extra_volumes/client_extra_volumes, Error at both severities) catches typos when the API server itself will not. Lists the reserved volume names/suffixes and mount-path prefixes (noting /etc/ssl and /etc/pki are deliberately not reserved), and that mounts land on the weka container only, never init containers. Covers propagation from WekaCluster/WekaClient onto owned WekaContainer specs, and the pod-config-hash rollout mechanics: pods are deleted and recreated (never patched) when the hash changes, unused digests are omitempty so non-adopters see no churn on operator upgrade, and pods that predate pod-config-hash tracking are adopted without rolling — so the first extraVolumes change on such a pod silently does nothing until it's deleted once or allowRotateNonAnnotatedPodConfigHash is set. Includes worked YAML for a cluster CA-bundle mount, a client-only (joinIpPorts, no targetCluster) OS-trust-store mount, and manager.extraVolumes for the operator's own Deployment.</summary>
</file>
Comment on lines +30 to +40
}

func CalcClientPodConfigVersion(spec *weka.WekaClientSpec) string {
inputs := ClientPodConfigVersionInputs{
PodConfigVersion: config.Config.PodConfigVersion,
Image: spec.Image,
ExtraVolumesDigest: resources.ExtraVolumesDigest(spec.ExtraVolumes),
ExtraVolumeMountsDigest: resources.ExtraVolumeMountsDigest(spec.ExtraVolumeMounts),
}
if config.Config.EnablePodConfigCodeVersionRotation {
inputs.WekaRuntimeVersion = consts.PodConfigCodeVersion

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolved wekaHome cacert secret is missing from the pod-config-version inputs, so changing it never rolls the pod.

updateContainerIfChanged (client_reconciler_loop.go:851) now propagates the resolved secret onto container.Spec.AdditionalSecrets, and AdditionalSecrets becomes a pod volume + volumeMount (resources/pod.go:815-828). But a pod is only recreated when container.Spec.PodConfigHash changes (funcs_pod_ensure.go:260-315targetPodConfigHash(container)), and PodConfigHash only changes when CalcClientPodConfigVersion changes.

Since WekaHomeCacertSecret isn't an input here:

  1. User sets/changes spec.wekaHome.cacertSecret (or the inherited cluster value changes).
  2. HandleSpecUpdates writes the new AdditionalSecrets onto every client container. ✅
  3. HandleUpgrade computes an unchanged targetPodConfigHash, hits the early return nil at line 1081, and no pod is ever deleted.
  4. The running pod keeps the old secret volume (or none at all), so the new CA is never mounted and weka_cloud_ca_cert_path still points at the old/absent bundle.

The comment at client_reconciler_loop.go:849-850 says the point of the propagation is so "a change reaches already-running containers" — as written it reaches the container spec but not the pod. Suggest adding the resolved secret name to the inputs:

	// WekaHomeCacertSecret is a pod-shaping input: it becomes a secret volume in the pod
	// (pod.go AdditionalSecrets), so a change has to roll the pod to take effect.
	WekaHomeCacertSecret string `json:"wekaHomeCacertSecret,omitempty"`

and passing it through from the caller (which already resolves it via domain.GetWekaHomeClientCacertSecret). Note this needs the resolved value, so CalcClientPodConfigVersion would have to take the target cluster (or the resolved string) rather than just *WekaClientSpec.

Worth adding a test for exactly this: change spec.wekaHome.cacertSecret on an existing client → assert PodConfigHash changes.

Comment on lines +1091 to +1107
if c.wekaClient.Status.LastAppliedPodConfigHash == "" && !config.Config.AllowRotateNonAnnotatedPodConfigHash {
logger.Info("Adopting current pod config version (first deploy)", "targetPodConfigHash", targetPodConfigHash)
// The containers are adopted too, not just the client. handleSpecVersionMismatch declines to
// roll a pod whose annotation and container status are both empty, so a client adopted only
// at the parent level would, on its next real config change, get podConfigHash patched onto
// its containers, never roll their pods, and sit in Upgrading forever.
for _, container := range c.containers {
if container.Status.LastAppliedPodConfigHash != "" {
continue
}
container.Status.LastAppliedPodConfigHash = targetPodConfigHash
if err := c.Status().Update(ctx, container); err != nil {
return fmt.Errorf("failed to adopt pod config version on container %s: %w", container.Name, err)
}
}
c.wekaClient.Status.LastAppliedPodConfigHash = targetPodConfigHash
return c.Status().Update(ctx, c.wekaClient)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The adoption branch can permanently swallow a pending image upgrade for clients that predate pod-config tracking.

HandleUpgrade previously ran NewUpgradeController(..., spec.Image, ""), i.e. purely image-tracked: isContainerAligned compared container.Spec.Image == TargetImage (upgrade/upgrade.go:34-40). Now the target hash is always non-empty, so alignment is hash-based only, and this branch adopts without rolling.

Failure scenario: an existing WekaClient has Status.LastAppliedPodConfigHash == "" (any client created before this change). The operator is upgraded and spec.image is bumped to B in the same apply, so the first reconcile of the new operator sees image B already in the spec:

  1. targetPodConfigHash = hash(B, …) — differs from "", so we don't return at line 1081.
  2. Status.LastAppliedPodConfigHash == "" → adoption fires, stamps hash(B) on the client and on every container's status, returns.
  3. Next reconcile: targetPodConfigHash == Status.LastAppliedPodConfigHashreturn nil at line 1081.

The containers still carry Spec.Image == A and Status.LastAppliedImage == A, and nothing ever patches them. The client silently stays on the old image with no Upgrading state and no event. Previously step 3 would have rolled it.

The wekacluster counterpart at least computes imageChanged := cluster.Spec.Image != cluster.Status.LastAppliedImage before adopting; suggest gating adoption the same way here, e.g. only adopt when no image drift is outstanding:

	imageDrift := slices.ContainsFunc(c.containers, func(ct *weka.WekaContainer) bool {
		return ct.Spec.Image != c.wekaClient.Spec.Image || ct.Status.LastAppliedImage != c.wekaClient.Spec.Image
	})
	if c.wekaClient.Status.LastAppliedPodConfigHash == "" && !imageDrift && !config.Config.AllowRotateNonAnnotatedPodConfigHash {

Two smaller points in the same branch:

  • The per-container c.Status().Update(ctx, container) loop races the wekacontainer controller, which also writes LastAppliedPodConfigHash. A conflict aborts the whole step mid-loop, leaving some containers adopted and some not; the client status is written last so the next reconcile re-enters and finishes, but a RetryOnConflict (or a Patch on the status subresource) would be less noisy.
  • Once Status.LastAppliedPodConfigHash is set, the early return at line 1081 means clearPrePullAnnotationForClient never runs again. Its failure at line 1114 is only logged as a warning, so a transient failure there leaves the weka.io/prepull-* annotation behind forever.

Comment on lines +849 to +854
// Propagate the resolved wekaHome cacertSecret so a change reaches already-running containers,
// not only ones created after the change (buildClientWekaContainer only runs on container-create).
if container.Spec.AdditionalSecrets["wekahome-cacert"] != newClientSpec.WekaHomeCacertSecret {
container.Spec.AdditionalSecrets = clientAdditionalSecrets(newClientSpec.WekaHomeCacertSecret)
changed = true
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replacing the whole map means any other AdditionalSecrets key on the container is silently dropped, even though the if only tested the wekahome-cacert key. Today the map only ever has that one key (as the comment in extra_volumes.go:29-30 notes), so this is latent rather than broken — but it will break quietly the moment a second key is added, and the reserved-names list already depends on that same "exactly one entry" assumption in a second place.

Safer to mutate just the one key:

	if container.Spec.AdditionalSecrets["wekahome-cacert"] != newClientSpec.WekaHomeCacertSecret {
		if container.Spec.AdditionalSecrets == nil {
			container.Spec.AdditionalSecrets = map[string]string{}
		}
		if newClientSpec.WekaHomeCacertSecret == "" {
			delete(container.Spec.AdditionalSecrets, "wekahome-cacert")
		} else {
			container.Spec.AdditionalSecrets["wekahome-cacert"] = newClientSpec.WekaHomeCacertSecret
		}
		changed = true
	}

Also: "wekahome-cacert" is now a bare literal in three places (here, clientAdditionalSecrets, factory/container_factory.go:96) plus a derived "wekahome-cacert-secret" in resources.ReservedVolumeNames. Worth a single exported const so the reserved-name entry can be derived from it rather than hand-copied.

Comment on lines +44 to +48
// Files, not directories: the /-boundary rule means "/opt/weka" does not cover a sibling
// like "/opt/weka_runtime.py", so each of these needs its own entry.
"/opt/weka_runtime.py", // pod.go: weka_runtime.py mount
"/usr/local/bin/wekaauthcli", // pod.go: wekaauthcli mount
"/devenv.sh", // drivers.go: COS dev-env script mount

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/usr/bin/weka is missing from this list.

init_containers.go:171 mounts the staged CLI onto container 0 at /usr/bin/weka (via copyWekaVersionToContainer, reached from setDriverDependencies when Spec.Instructions.Type == InstructionCopyWekaFilesToDriverLoader). ReservedMountPaths covers /usr/local/bin/weka and /usr/src, but neither /usr/bin nor /usr/bin/weka, so IsReservedMountPath("/usr/bin/weka") returns false.

Consequence: an extraVolumeMounts entry at /usr/bin/weka passes admission and the reserved check in applyExtraVolumes. On pods that take the copy path it then trips the dynamic existing-path check (a hard pod-build error rather than a clean admission rejection, since applyExtraVolumes runs at pod.go:941, after driver deps are set); on pods that don't, it is accepted and silently shadows the CLI the operator stages from the cluster image.

Suggested change
// Files, not directories: the /-boundary rule means "/opt/weka" does not cover a sibling
// like "/opt/weka_runtime.py", so each of these needs its own entry.
"/opt/weka_runtime.py", // pod.go: weka_runtime.py mount
"/usr/local/bin/wekaauthcli", // pod.go: wekaauthcli mount
"/devenv.sh", // drivers.go: COS dev-env script mount
// Files, not directories: the /-boundary rule means "/opt/weka" does not cover a sibling
// like "/opt/weka_runtime.py", so each of these needs its own entry.
"/opt/weka_runtime.py", // pod.go: weka_runtime.py mount
"/usr/local/bin/wekaauthcli", // pod.go: wekaauthcli mount
"/usr/bin/weka", // init_containers.go: staged weka CLI mount
"/devenv.sh", // drivers.go: COS dev-env script mount

Separately: these two lists are hand-maintained copies of what pod.go/drivers.go/init_containers.go actually create, and they will drift (three entries — devenv, google-cloud-key, host-usr-src — already reserve names nothing creates anymore; the live equivalents are weka-boot-scripts, gcloud-credentials, usrsrc). A unit test that builds a pod per mode/config combination and asserts every produced volume name and container-0 mount path is covered by these lists would catch the next drift, and would have caught /usr/bin/weka.

Comment on lines +119 to +128
func ExtraVolumeMountsDigest(mounts []corev1.VolumeMount) string {
if len(mounts) == 0 {
return ""
}
data, err := json.Marshal(mounts)
if err != nil {
return ""
}
return util.GetHash(string(data), 16)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning "" on marshal failure collides with the "no mounts at all" return two lines above, and "" is exactly the value the callers treat as unset. So a marshal error makes updateContainerIfChanged (client_reconciler_loop.go:699) and HandleSpecUpdates (funcs_upgrade.go:379) conclude "nothing to propagate" and skip the update — a silently dropped config rather than a loud failure. It also feeds CalcClientPodConfigVersion, so the hash would silently not reflect the mounts.

json.Marshal of []corev1.VolumeMount can't realistically fail, which is exactly why swallowing the error buys nothing. Per CLAUDE.md ("never swallow errors"), either return the error, or make the impossible case distinguishable from unset:

func ExtraVolumeMountsDigest(mounts []corev1.VolumeMount) string {
	if len(mounts) == 0 {
		return ""
	}
	data, err := json.Marshal(mounts)
	if err != nil {
		// Unreachable for a plain struct slice; must not collapse to "" (== unset), which
		// would make callers skip propagation instead of failing.
		panic(fmt.Sprintf("marshaling extraVolumeMounts: %v", err))
	}
	return util.GetHash(string(data), 16)
}

Same shape applies to the json.Marshal at line 105 in NormalizeExtraVolumes — there the //nolint:errcheck rationale ("volumes just decoded from JSON") is sound, but the result is assigned even if normalized were nil, which would write Raw: nil onto the container spec. Cheap to assert instead of assume.

Comment on lines +3192 to +3210
if [ -d /var/run/secrets/weka-operator/wekahome-cacert ]; then
rm -rf /opt/weka/k8s-runtime/vars/wh-cacert
mkdir -p /opt/weka/k8s-runtime/vars/wh-cacert/
cp /var/run/secrets/weka-operator/wekahome-cacert/cert.pem /opt/weka/k8s-runtime/vars/wh-cacert/cert.pem
chmod 400 /opt/weka/k8s-runtime/vars/wh-cacert/cert.pem
# Secret data-key names are arbitrary, so concatenate every mounted PEM rather
# than assuming one is named cert.pem (the glob skips the ..data/..2025_* dotfiles).
for f in /var/run/secrets/weka-operator/wekahome-cacert/*; do
[ -f "$f" ] || continue
cat "$f" >> /opt/weka/k8s-runtime/vars/wh-cacert/cert.pem
echo "" >> /opt/weka/k8s-runtime/vars/wh-cacert/cert.pem
done
# Test for actual PEM content, not file size: the separator above writes a newline
# per key, so a secret holding only empty or non-PEM values still yields a non-empty
# file. An explicit CA replaces the system trust store, so pointing
# weka_cloud_ca_cert_path at a certificate-less file breaks Weka Home silently.
if grep -q "BEGIN CERTIFICATE" /opt/weka/k8s-runtime/vars/wh-cacert/cert.pem 2>/dev/null; then
chmod 400 /opt/weka/k8s-runtime/vars/wh-cacert/cert.pem
else
rm -rf /opt/weka/k8s-runtime/vars/wh-cacert
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things here.

1. Concatenating every file in the secret dir will inline a private key if the user points cacertSecret at a kubernetes.io/tls secret. That's a very natural mistake — a TLS secret is the obvious thing to reach for, and it has tls.crt + tls.key. The BEGIN CERTIFICATE check passes (thanks to tls.crt), so tls.key lands verbatim inside /opt/weka/k8s-runtime/vars/wh-cacert/cert.pem, which is then pointed at by weka_cloud_ca_cert_path. Worth filtering to certificate blocks, or at least skipping files containing a private key:

for f in /var/run/secrets/weka-operator/wekahome-cacert/*; do
    [ -f "$f" ] || continue
    grep -q "BEGIN CERTIFICATE" "$f" || continue
    grep -q "PRIVATE KEY" "$f" && continue
    cat "$f" >> /opt/weka/k8s-runtime/vars/wh-cacert/cert.pem
    echo "" >> /opt/weka/k8s-runtime/vars/wh-cacert/cert.pem
done

That also removes the need for the post-hoc content test, since a dir with no PEM certs produces no file at all.

2. chmod 400 happens only after the loop, so the file exists with the default umask (typically 0644) for the duration of the concatenation. Harmless for a public CA bundle, but combined with (1) it's a real window on a private key. Set the mode before writing:

: > /opt/weka/k8s-runtime/vars/wh-cacert/cert.pem
chmod 400 /opt/weka/k8s-runtime/vars/wh-cacert/cert.pem

(then drop the trailing chmod, keeping only the rm -rf on the no-certs path).

Nit on the comment: "the glob skips the ..data/..2025_* dotfiles" — true, but the reason is that shell globs don't match leading dots, not the date; the ..2025_* naming will read as stale in a year. # the glob skips the secret's ..data/..timestamp dotfiles is enough.

Comment on lines +59 to +100
## Reserved names and paths

The operator reserves certain volume names, name suffixes, and mount-path
prefixes for its own use inside weka/client pods. An `extraVolumes` entry or
`extraVolumeMounts` entry that collides is rejected — at admission time if
enabled, and always by the pod factory as a final backstop. The authoritative
lists live in
[`internal/controllers/resources/extra_volumes.go`](../../../internal/controllers/resources/extra_volumes.go).

**Reserved volume names** (`ReservedVolumeNames`) — every volume name the
operator itself assigns anywhere in a weka pod (backend, client, or init
container). Volumes are pod-scoped, so a name used only by an init container
is reserved too, even though extra mounts never land in init containers (see
[Mount scope](#mount-scope-weka-container-only) below):

```
osrelease, dev, run, sys, weka-boot-scripts, hugepages, smbw-shm,
host-shared-netns, weka-container-persistence-dir, weka-container-shared-dir,
weka-cluster-persistence-dir, weka-container-global-persistence-dir,
weka-proxy-socket-dir, weka-ssdproxy-local-socket, node-info, weka-credentials,
proc-sysrq-trigger, proc-cmdline, devenv, google-cloud-key, host-modules,
host-usr-src, shared-weka-version, otel-packages, libmodules, usrsrc,
gcloud-credentials, wekahome-cacert-secret
```

`wekahome-cacert-secret` is the one name in that list the operator derives at
runtime rather than hardcodes — it comes from `spec.additionalSecrets`, which
forms `<name>-secret`. Only that literal name is reserved, so an ordinary user
name like `corp-ca-secret` is fine.

**Reserved mount-path prefixes** (`ReservedMountPathPrefixes`) — a mount
cannot land on or under:

```
/dev, /sys, /host, /host-binds, /hostside, /opt/weka,
/opt/weka-global-persistence, /var/run/secrets/weka-operator,
/usr/local/bin/weka, /etc/wekaio, /etc/syslog-ng,
/shared-python-packages, /shared-weka-version, /var/log, /lib/modules,
/usr/src, /var/secrets/google
```

**Reserved exact paths** (`ReservedMountPaths`) — operator mounts that are

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section describes an API that doesn't exist, which will send readers looking for symbols they can't find.

  1. ReservedMountPathPrefixes (line 89) and "Reserved exact paths" (line 100)extra_volumes.go:38-50 has a single ReservedMountPaths slice, and IsReservedMountPath applies the same on-or-under rule to every entry. There is no prefixes/exact split. The distinction the code actually makes is that a file entry (/opt/weka_runtime.py) reserves only itself simply because nothing lives under it — not because it's matched differently.

  2. "name suffixes" (line 61) — no suffix reservation exists. extra_volumes.go:29-32 explicitly rejects that approach in favour of reserving the one literal derived name, which line 84-88 then correctly explains. The two statements contradict each other.

  3. Line 84 says wekahome-cacert-secret "comes from spec.additionalSecrets" — it's the WekaContainer's spec.additionalSecrets, which users don't set; the operator populates it from spec.wekaHome.cacertSecret. Worth saying so, since as written it reads like a user-facing field.

Suggest collapsing 89-100 into one "Reserved mount paths (ReservedMountPaths) — a mount cannot land on, or under, any of:" list with the full 13 entries, and dropping "name suffixes" from line 61.

|---|---|---|
| WekaCluster | `spec.wekaHome.cacertSecret` | Mounts the Secret into every backend pod, stages it at `/opt/weka/k8s-runtime/vars/wh-cacert/cert.pem`, and sets the cluster-wide `weka_cloud_ca_cert_path` from a drive container. |
| WekaClient | `spec.wekaHome.cacertSecret` | Places the same file on the client pod at the same path. When `targetCluster` is set and the cluster is in the **same namespace**, this is **derived automatically from the target cluster's own `cacertSecret`** — set it explicitly only to override that default. A cluster in another namespace is not inherited from: only the Secret *name* would be copied, and it would not resolve in the client's namespace, so the client emits a warning event and you must set the field yourself. |
| Operator (Helm) | `wekahome.cacertSecret` | Used only by the operator's own CR reporter (the process that periodically reports CRs to Weka Home). It reads the Secret through the Kubernetes API into an in-memory certificate pool at request time. **No volume, no mount, no container filesystem is involved.** |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Used only by the operator's own CR reporter … No volume, no mount, no container filesystem is involved" is not correct, including for the code in this PR.

The Helm wekahome.cacertSecret lands in env.Config.WekaHome.CacertSecret, and that is:

  • the last-resort fallback returned by GetWekaHomeClientCacertSecret (internal/pkg/domain/wekahome.go:86), which this PR wires into AdditionalSecrets → a secret volume + mount on every client pod;
  • copied into WekaHomeConfig.CacertSecret by GetWekahomeConfig (wekahome.go:32-34), which container_factory.go:87-98 turns into additionalSecrets["wekahome-cacert"] → a secret volume + mount on every backend pod.

So it is a genuine cluster-wide/client-wide default that is mounted, not a reporter-only knob. Someone reading this table would set the Helm value expecting no pod impact and get every weka pod remounted (and rolled, once the pod-config-hash gap is fixed).

Also on line 85 (WekaClient row): the precedence chain is described as client → same-namespace cluster, but omits the third step. Per wekahome.go:79-87 it's actually:

  1. wekaClient.spec.wekaHome.cacertSecret
  2. target cluster's spec.wekaHome.cacertSecret, only if same namespace
  3. Helm wekahome.cacertSecret (the operator-wide default)

Worth stating all three explicitly, since (3) is what a cross-namespace client silently falls back to after the warning event.

One more omission worth a sentence somewhere in this file: weka_runtime.py:3202-3210 deletes the staged wh-cacert directory when the concatenated file contains no BEGIN CERTIFICATE. That's a deliberate and good safety valve, but from the operator's point of view it's a silent fall back to the OS trust store — the pod starts fine and nothing surfaces the misconfigured Secret. Readers debugging "I set cacertSecret and nothing changed" need to know to check that file's existence.

Comment on lines +70 to +87
clusterSecret := ""
if sameNamespace && targetCluster.Spec.WekaHome != nil {
clusterSecret = targetCluster.Spec.WekaHome.CacertSecret
}

if clientSecret != "" {
return clientSecret, false
}
if clusterSecret != "" {
return clusterSecret, false
}

if !sameNamespace && targetCluster != nil && targetCluster.Spec.WekaHome != nil && targetCluster.Spec.WekaHome.CacertSecret != "" {
crossNamespaceSkipped = true
}

return env.Config.WekaHome.CacertSecret, crossNamespaceSkipped
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The crossNamespaceSkipped return is only consumed in buildClientWekaContainer, which runs on container creation (client_reconciler_loop.go:485-495). NewUpdatableClientSpec — the path taken on every reconcile of an existing client — discards it with whCaCert, _ := … (line 1377). So the "you must set cacertSecret yourself" warning event never reaches the users most likely to need it: someone who adds spec.wekaHome.cacertSecret to an existing cross-namespace cluster and wonders why their clients didn't pick it up. Emitting it from the reconcile path (still throttled) would fix that.

Small cleanup in the same function: the final if re-derives what sameNamespace already told you. Since clusterSecret is only populated when sameNamespace, reaching line 84 with a non-empty cluster secret implies !sameNamespace:

	if clientSecret != "" {
		return clientSecret, false
	}
	if clusterSecret != "" {
		return clusterSecret, false
	}
	// Only reachable with !sameNamespace, since clusterSecret is populated only when same-namespace.
	if targetCluster != nil && targetCluster.Spec.WekaHome != nil && targetCluster.Spec.WekaHome.CacertSecret != "" {
		crossNamespaceSkipped = true
	}
	return env.Config.WekaHome.CacertSecret, crossNamespaceSkipped

Also worth noting for the test suite: wekahome_test.go:70-79 uses same-secret for both the client and the cluster value, so it can't distinguish "client wins" from "cluster wins" — it passes under either precedence. Use distinct values there.

Comment on lines +66 to 72
{{- range .Values.manager.extraVolumes }}
{{- if eq .name "tmpdir" }}{{ fail "manager.extraVolumes: the volume name \"tmpdir\" is reserved by the operator" }}{{ end }}
{{- end }}
{{- range .Values.manager.extraVolumeMounts }}
{{- if eq .mountPath "/tmp" }}{{ fail "manager.extraVolumeMounts: the mount path \"/tmp\" is reserved by the operator" }}{{ end }}
{{- end }}
volumes:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The /tmp guard is exact-match only, so mountPath: /tmp/foo (or /tmp/) sails through and shadows part of the operator's own tmpdir emptyDir. Same on-or-under rule the Go side uses (IsReservedMountPath) would be more consistent:

      {{- range .Values.manager.extraVolumeMounts }}
      {{- if or (eq .mountPath "/tmp") (hasPrefix "/tmp/" .mountPath) }}{{ fail "manager.extraVolumeMounts: the mount path \"/tmp\" is reserved by the operator" }}{{ end }}
      {{- end }}

Also nothing here catches duplicate names or duplicate mount paths within the user's own lists — Kubernetes will reject the Deployment, but with a much less obvious message than the two fails above. Low priority given the failure is at least loud.

Comment on lines +77 to +104
for i, m := range mounts {
idxPath := mountsPath.Index(i)

if _, ok := declaredNames[m.Name]; !ok {
errs = append(errs, field.Invalid(idxPath.Child("name"), m.Name,
"does not match any entry in extraVolumes; mounting an operator-managed base "+
"volume at a second path is not supported"))
}

switch {
case !path.IsAbs(m.MountPath):
errs = append(errs, field.Invalid(idxPath.Child("mountPath"), m.MountPath,
"must be an absolute path"))
case path.Clean(m.MountPath) != m.MountPath:
errs = append(errs, field.Invalid(idxPath.Child("mountPath"), m.MountPath,
fmt.Sprintf("must be a cleaned path; use %q", path.Clean(m.MountPath))))
default:
if resources.IsReservedMountPath(m.MountPath) {
errs = append(errs, field.Invalid(idxPath.Child("mountPath"), m.MountPath,
"is reserved for an operator-managed mount"))
}
if _, dup := seenPaths[m.MountPath]; dup {
errs = append(errs, field.Duplicate(idxPath.Child("mountPath"), m.MountPath))
}
}
seenPaths[m.MountPath] = struct{}{}
}
return errs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two small inconsistencies with the pod-factory backstop this mirrors:

  1. seenPaths is keyed on the raw m.MountPath (line 103, outside the switch), whereas applyExtraVolumes keys its collision map on path.Clean(m.MountPath) (pod.go:975). In practice the un-cleaned cases already error out via the path.Clean branch, so this can't produce a wrong result today — but keying on path.Clean(m.MountPath) here would make the two implementations obviously equivalent instead of accidentally equivalent.

  2. raw.Object is ignored. validateExtraVolumes short-circuits on len(raw.Raw) == 0, and NormalizeExtraVolumes collapses that same case to nil. A RawExtension can legitimately carry Object with an empty Raw; that never happens for objects decoded from an admission request or read back from the API server, so this is theoretical — but if it ever did, the volumes would be silently dropped rather than rejected. A raw.Object != nil guard returning an "unsupported form" error would make that explicit.

Also, line 46 embeds the entire raw JSON blob into the error value: for a large extraVolumes that produces an unwieldy admission message. Truncating, or just reporting the decoder error without the payload, reads better.

| hugepages | int | hugepages, value in megabytes |
| hugepagesOffset | *int | value in megabytes to offset |
| wekaHomeConfig | WekahomeClientConfig | DEPRECATED, kept for compatibility with old API clients, not taking any action, to be removed on new API version |
| wekaHome | *WekahomeClientConfig | |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads-up on something riding along in the pkg/weka-k8s-api submodule bump rather than in this PR's own Go code: the deprecated wekaHomeConfig field is removed from WekaClientSpec (also visible in doc/api_dump/api-schema.json at the WekaHomeConfig/wekaHomeConfig deletion).

Its doc comment said "kept for compatibility with old API clients … to be removed on new API version" — but this removes it from v1alpha1, i.e. within the same API version. Consequences for anyone still setting it:

  • Plain kubectl apply / client-go: pruned silently (behaviour unchanged, since the field was already a no-op).
  • Server-side apply, and kubectl apply --validate=strict: hard rejection on the unknown field.
  • Any stored object still carrying it gets it pruned on next write.

If that's intentional and coordinated, fine — but it's an unrelated breaking API change in a PR titled fix:, so it deserves an explicit callout in the PR description / release notes rather than only showing up as a line deleted from a generated doc.

Comment on lines +342 to +372
// the containers, still fails to roll their pods, and strands the client after all.
func TestHandleUpgrade_AdoptsPodConfigHashWithoutRolling(t *testing.T) {
scheme := runtime.NewScheme()
if err := weka.AddToScheme(scheme); err != nil {
t.Fatalf("failed to add weka scheme: %v", err)
}

wekaClient := &weka.WekaClient{
ObjectMeta: metav1.ObjectMeta{Name: "test-client", Namespace: "default"},
Spec: weka.WekaClientSpec{Image: "quay.io/weka.io/weka-in-container:4.5.0"},
}
// Pre-existing container: hash never set, pod predates the annotation.
container := &weka.WekaContainer{
ObjectMeta: metav1.ObjectMeta{Name: "test-client-container", Namespace: "default"},
Spec: weka.WekaContainerSpec{Image: wekaClient.Spec.Image},
}

fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(wekaClient, container).
WithStatusSubresource(wekaClient, container).
Build()

c := &clientReconcilerLoop{
Client: fakeClient,
Recorder: record.NewFakeRecorder(10),
wekaClient: wekaClient,
containers: []*weka.WekaContainer{container},
}

ctx := context.Background()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good test for the case it covers, and the doc comment is genuinely useful. Two gaps worth closing given how much behaviour this branch now gates:

  1. Image changed at the same time as adoption. Here container.Spec.Image == wekaClient.Spec.Image, so the scenario I flagged on client_reconciler_loop.go:1091 isn't exercised. A second case with container.Spec.Image = "…:4.4.0" / wekaClient.Spec.Image = "…:4.5.0" should assert the container does get patched (or at minimum that the client doesn't end up permanently short-circuited on the old image). As written that case would silently pass adoption and never roll.

  2. AllowRotateNonAnnotatedPodConfigHash = true — the documented escape hatch for exactly this transition is untested; a case asserting the upgrade controller path is taken instead of adoption would pin it.

Also, the assertion on c.upgradeInProgress at the end doesn't test anything: nothing on the adoption path ever sets it, so it holds regardless of whether adoption or the upgrade path ran. Asserting on container.Spec.PodConfigHash still being "" (the "without rolling" half of the test's own name) would be the load-bearing check.

Neighbouring nits in this file's other new cases:

  • TestNewUpdatableClientSpec_HashStructSurvivesCsiVolumeAttributesMap is a genuinely valuable regression test — worth keeping the comment.
  • wekahome_test.go:70-79 ("client value wins when it matches the cluster") sets both to same-secret, so it passes under either precedence; use distinct values.
  • client_wekahome_cacert_unverifiable_test.go only counts errors. The validator's warning severity comes from admission/defaults.go:50 and is what makes this rule non-blocking — that mapping is the thing worth asserting, since flipping it to Error would break every client-only deployment using a private CA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants