Context
pkg/primitives/statefulset/mutator.go and pkg/primitives/deployment/mutator.go
expose an essentially identical editing surface for env/container/podspec/metadata
work: EditContainers, EditInitContainers, EditPodSpec,
EditPodTemplateMetadata, EditObjectMetadata, EnsureContainerEnvVar,
EnsureContainerArg, EnsureReplicas, RemoveContainer*, etc. They diverge only
in the spec editor (EditStatefulSetSpec vs EditDeploymentSpec) and the
StatefulSet-only VolumeClaimTemplate methods.
However *statefulset.Mutator and *deployment.Mutator are unrelated concrete
types, and the only framework interface spanning them, generic.FeatureMutator,
covers just Apply() and NextFeature() — not the editing methods.
The problem
A consumer that wants to write a workload-kind-agnostic mutation — e.g. a
shared "emit these auth/license/storage env vars on the app container" helper
that some components render as a StatefulSet (zeebe) and others as a Deployment
(gateway, operate, tasklist, identity, connectors) — cannot express it against a
framework type. There is no shared static type carrying EditContainers /
EnsureContainerEnvVar, so the helper either has to be duplicated per workload
kind or routed through a consumer-defined structural interface.
This surfaced building the Camunda orchestration cluster in camunda-operator:
five components share the same env-emission concerns but render as two different
workload kinds, and we wanted one set of emitters returning one mutation type.
Workarounds today (all unsatisfying)
- Define a local structural interface (
WorkloadMutator) listing the shared
methods, plus per-kind adapter funcs (AsSTS, AsDeploy) that wrap a
Mutation[WorkloadMutator] into a statefulset.Mutation / deployment.Mutation.
Works (Go structural typing means both concrete mutators satisfy it), but the
interface is a hand-maintained mirror of the framework's method set: a
framework bump that renames/adds a method silently drifts until a compile-time
var _ WorkloadMutator = (*statefulset.Mutator)(nil) assertion catches it.
- Duplicate every shared emitter per workload kind. Defeats the point of shared
emitters and doubles the per-mutation test surface.
Proposed solution shape
Export a framework interface (say primitives.WorkloadMutator or
generic.PodWorkloadMutator) that both *statefulset.Mutator and
*deployment.Mutator declare they implement, covering the shared
container/podspec/metadata/env editing methods. Optionally a small generic
adapter so a feature.Mutation[WorkloadMutator] can be lifted into a
statefulset.Mutation / deployment.Mutation without per-consumer boilerplate.
Open question
Should the shared interface be the full intersection of the two method sets, or a
deliberately minimal "env + container + metadata" subset (the spec-editor and
VolumeClaimTemplate methods stay kind-specific anyway)? A minimal subset keeps the
contract stable across primitives that may later host more workload kinds (Job,
DaemonSet), at the cost of consumers reaching for the concrete type for less-common
edits.
Context
pkg/primitives/statefulset/mutator.goandpkg/primitives/deployment/mutator.goexpose an essentially identical editing surface for env/container/podspec/metadata
work:
EditContainers,EditInitContainers,EditPodSpec,EditPodTemplateMetadata,EditObjectMetadata,EnsureContainerEnvVar,EnsureContainerArg,EnsureReplicas,RemoveContainer*, etc. They diverge onlyin the spec editor (
EditStatefulSetSpecvsEditDeploymentSpec) and theStatefulSet-only VolumeClaimTemplate methods.
However
*statefulset.Mutatorand*deployment.Mutatorare unrelated concretetypes, and the only framework interface spanning them,
generic.FeatureMutator,covers just
Apply()andNextFeature()— not the editing methods.The problem
A consumer that wants to write a workload-kind-agnostic mutation — e.g. a
shared "emit these auth/license/storage env vars on the app container" helper
that some components render as a StatefulSet (zeebe) and others as a Deployment
(gateway, operate, tasklist, identity, connectors) — cannot express it against a
framework type. There is no shared static type carrying
EditContainers/EnsureContainerEnvVar, so the helper either has to be duplicated per workloadkind or routed through a consumer-defined structural interface.
This surfaced building the Camunda orchestration cluster in camunda-operator:
five components share the same env-emission concerns but render as two different
workload kinds, and we wanted one set of emitters returning one mutation type.
Workarounds today (all unsatisfying)
WorkloadMutator) listing the sharedmethods, plus per-kind adapter funcs (
AsSTS,AsDeploy) that wrap aMutation[WorkloadMutator]into astatefulset.Mutation/deployment.Mutation.Works (Go structural typing means both concrete mutators satisfy it), but the
interface is a hand-maintained mirror of the framework's method set: a
framework bump that renames/adds a method silently drifts until a compile-time
var _ WorkloadMutator = (*statefulset.Mutator)(nil)assertion catches it.emitters and doubles the per-mutation test surface.
Proposed solution shape
Export a framework interface (say
primitives.WorkloadMutatororgeneric.PodWorkloadMutator) that both*statefulset.Mutatorand*deployment.Mutatordeclare they implement, covering the sharedcontainer/podspec/metadata/env editing methods. Optionally a small generic
adapter so a
feature.Mutation[WorkloadMutator]can be lifted into astatefulset.Mutation/deployment.Mutationwithout per-consumer boilerplate.Open question
Should the shared interface be the full intersection of the two method sets, or a
deliberately minimal "env + container + metadata" subset (the spec-editor and
VolumeClaimTemplate methods stay kind-specific anyway)? A minimal subset keeps the
contract stable across primitives that may later host more workload kinds (Job,
DaemonSet), at the cost of consumers reaching for the concrete type for less-common
edits.