diff --git a/docs/custom-resource.md b/docs/custom-resource.md index ed3ef656..195ae24a 100644 --- a/docs/custom-resource.md +++ b/docs/custom-resource.md @@ -434,9 +434,12 @@ func NewBuilder(gs *examplev1.GameServer) *Builder { return &Builder{base: base} } -// WithMutation registers a feature-gated mutation. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +// WithMutation registers one or more feature-gated mutations, applied in the order given. +// Pass a slice with the spread operator: b.WithMutation(factory()...) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } @@ -638,6 +641,29 @@ func CompetitiveMode(version string, enabled bool) gameserver.Mutation { }, } } + +// DefaultSettings returns baseline mutations applied to every GameServer regardless of feature flags. +// The version parameter is forwarded to any version-aware mutations in the set. +func DefaultSettings(version string) []gameserver.Mutation { + return []gameserver.Mutation{ + { + Name: "default-replicas", + Feature: nil, // always applied + Mutate: func(m *gameserver.Mutator) error { + m.SetReplicas(1) + return nil + }, + }, + { + Name: "default-max-players", + Feature: feature.NewVersionGate(version, nil), + Mutate: func(m *gameserver.Mutator) error { + m.SetMaxPlayers(100) + return nil + }, + }, + } +} ``` Mutations are applied in registration order. When a mutation's `Feature` is nil or reports `Enabled() == true`, its @@ -663,6 +689,7 @@ func buildGameComponent(owner *MyOperatorCR) (*component.Component, error) { res, err := gameserver.NewBuilder(gs). WithMutation(features.HighCapacityMode(owner.Spec.Version)). WithMutation(features.CompetitiveMode(owner.Spec.Version, owner.Spec.Competitive)). + WithMutation(features.DefaultSettings(owner.Spec.Version)...). // spread a []Mutation slice Build() if err != nil { return nil, err diff --git a/docs/primitives.md b/docs/primitives.md index 043a8bfd..93e19ef7 100644 --- a/docs/primitives.md +++ b/docs/primitives.md @@ -134,6 +134,26 @@ This design: - **Guarantees ordering**: features apply in registration order; within a feature, categories apply in a fixed sequence - **Avoids error-prone slice manipulation**: editors handle presence operations and stable selection internally +### Registering multiple mutations + +`WithMutation` is variadic, so a single call can register several mutations, applied in the order given: + +```go +b.WithMutation(first, second, third) +``` + +This composes cleanly with factories that return `[]Mutation`, without breaking the fluent chain: + +```go +return statefulset.NewBuilder(base). + WithMutation(defaults.ContainerImage(version, registry)). + WithMutation(defaults.ClusterEnv(cc)...). + WithMutation(defaults.ExporterEnv(version)...). + Build() +``` + +Calling `WithMutation()` with no arguments is a no-op. + ## Mutation Editors Editors provide scoped, typed APIs for modifying specific parts of a resource. Every editor exposes a `.Raw()` method diff --git a/pkg/generic/builder_base.go b/pkg/generic/builder_base.go index 19d0adfb..49601836 100644 --- a/pkg/generic/builder_base.go +++ b/pkg/generic/builder_base.go @@ -56,9 +56,11 @@ func (b *BaseBuilder[T, M]) InitBase( } } -// WithMutation registers a typed feature mutation for the resource. -func (b *BaseBuilder[T, M]) WithMutation(m Mutation[M]) { - b.BaseRes.Mutations = append(b.BaseRes.Mutations, m) +// WithMutation registers one or more typed feature mutations for the resource. +// Mutations are appended in the order provided; calling it with no arguments is +// a no-op. +func (b *BaseBuilder[T, M]) WithMutation(ms ...Mutation[M]) { + b.BaseRes.Mutations = append(b.BaseRes.Mutations, ms...) } // WithGuard registers a guard precondition for the resource. diff --git a/pkg/generic/builder_integration.go b/pkg/generic/builder_integration.go index e23c24ad..9cd51d23 100644 --- a/pkg/generic/builder_integration.go +++ b/pkg/generic/builder_integration.go @@ -42,11 +42,12 @@ func NewIntegrationBuilder[T client.Object, M FeatureMutator]( return b } -// WithMutation registers a typed feature mutation for the integration. +// WithMutation registers one or more typed feature mutations for the +// integration, in the order provided. func (b *IntegrationBuilder[T, M]) WithMutation( - m Mutation[M], + ms ...Mutation[M], ) *IntegrationBuilder[T, M] { - b.BaseBuilder.WithMutation(m) + b.BaseBuilder.WithMutation(ms...) return b } diff --git a/pkg/generic/builder_static.go b/pkg/generic/builder_static.go index 1f308e47..3d4f3a79 100644 --- a/pkg/generic/builder_static.go +++ b/pkg/generic/builder_static.go @@ -35,9 +35,10 @@ func NewStaticBuilder[T client.Object, M FeatureMutator]( return b } -// WithMutation registers a typed feature mutation for the static resource. -func (b *StaticBuilder[T, M]) WithMutation(m Mutation[M]) *StaticBuilder[T, M] { - b.BaseBuilder.WithMutation(m) +// WithMutation registers one or more typed feature mutations for the static +// resource, in the order provided. +func (b *StaticBuilder[T, M]) WithMutation(ms ...Mutation[M]) *StaticBuilder[T, M] { + b.BaseBuilder.WithMutation(ms...) return b } diff --git a/pkg/generic/builder_static_test.go b/pkg/generic/builder_static_test.go index 4e5bc09e..beb84746 100644 --- a/pkg/generic/builder_static_test.go +++ b/pkg/generic/builder_static_test.go @@ -46,6 +46,56 @@ func TestStaticBuilder(t *testing.T) { assert.Len(t, res.Mutations, 1) }) + t.Run("registers multiple mutations in order", func(t *testing.T) { + first := Mutation[*mockMutator]{ + Name: "first", + Feature: alwaysEnabled{}, + Mutate: func(_ *mockMutator) error { return nil }, + } + second := Mutation[*mockMutator]{ + Name: "second", + Feature: alwaysEnabled{}, + Mutate: func(_ *mockMutator) error { return nil }, + } + + builder := NewStaticBuilder(obj, identityFunc, newMutator) + result := builder.WithMutation(first, second) + require.Same(t, builder, result) + + res, err := result.Build() + require.NoError(t, err) + require.Len(t, res.Mutations, 2) + assert.Equal(t, "first", res.Mutations[0].Name) + assert.Equal(t, "second", res.Mutations[1].Name) + }) + + t.Run("spread slice registers all mutations in order", func(t *testing.T) { + muts := []Mutation[*mockMutator]{ + {Name: "a", Feature: alwaysEnabled{}, Mutate: func(_ *mockMutator) error { return nil }}, + {Name: "b", Feature: alwaysEnabled{}, Mutate: func(_ *mockMutator) error { return nil }}, + {Name: "c", Feature: alwaysEnabled{}, Mutate: func(_ *mockMutator) error { return nil }}, + } + + res, err := NewStaticBuilder(obj, identityFunc, newMutator). + WithMutation(muts...). + Build() + require.NoError(t, err) + require.Len(t, res.Mutations, 3) + assert.Equal(t, "a", res.Mutations[0].Name) + assert.Equal(t, "b", res.Mutations[1].Name) + assert.Equal(t, "c", res.Mutations[2].Name) + }) + + t.Run("zero mutations is a no-op", func(t *testing.T) { + builder := NewStaticBuilder(obj, identityFunc, newMutator) + result := builder.WithMutation() + require.Same(t, builder, result) + + res, err := result.Build() + require.NoError(t, err) + assert.Empty(t, res.Mutations) + }) + t.Run("cluster-scoped build succeeds without namespace", func(t *testing.T) { clusterObj := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{Name: "cluster-obj"}, diff --git a/pkg/generic/builder_task.go b/pkg/generic/builder_task.go index c29b89ca..1d2453a5 100644 --- a/pkg/generic/builder_task.go +++ b/pkg/generic/builder_task.go @@ -35,11 +35,12 @@ func NewTaskBuilder[T client.Object, M FeatureMutator]( return b } -// WithMutation registers a typed feature mutation for the task. +// WithMutation registers one or more typed feature mutations for the task, +// in the order provided. func (b *TaskBuilder[T, M]) WithMutation( - m Mutation[M], + ms ...Mutation[M], ) *TaskBuilder[T, M] { - b.BaseBuilder.WithMutation(m) + b.BaseBuilder.WithMutation(ms...) return b } diff --git a/pkg/generic/builder_workload.go b/pkg/generic/builder_workload.go index eee1f2e3..ff90a569 100644 --- a/pkg/generic/builder_workload.go +++ b/pkg/generic/builder_workload.go @@ -45,11 +45,12 @@ func NewWorkloadBuilder[T client.Object, M FeatureMutator]( return b } -// WithMutation registers a typed feature mutation for the workload. +// WithMutation registers one or more typed feature mutations for the workload, +// in the order provided. func (b *WorkloadBuilder[T, M]) WithMutation( - m Mutation[M], + ms ...Mutation[M], ) *WorkloadBuilder[T, M] { - b.BaseBuilder.WithMutation(m) + b.BaseBuilder.WithMutation(ms...) return b } diff --git a/pkg/primitives/clusterrole/builder.go b/pkg/primitives/clusterrole/builder.go index 7250febe..884b153a 100644 --- a/pkg/primitives/clusterrole/builder.go +++ b/pkg/primitives/clusterrole/builder.go @@ -41,13 +41,15 @@ func NewBuilder(cr *rbacv1.ClusterRole) *Builder { return &Builder{base: sb} } -// WithMutation registers a mutation for the ClusterRole. +// WithMutation registers one or more mutations for the ClusterRole. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // A mutation with a nil Feature is applied unconditionally; one with a non-nil // Feature is applied only when that feature is enabled. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/clusterrolebinding/builder.go b/pkg/primitives/clusterrolebinding/builder.go index b30cc91a..3334e06b 100644 --- a/pkg/primitives/clusterrolebinding/builder.go +++ b/pkg/primitives/clusterrolebinding/builder.go @@ -44,13 +44,15 @@ func NewBuilder(crb *rbacv1.ClusterRoleBinding) *Builder { } } -// WithMutation registers a mutation for the ClusterRoleBinding. +// WithMutation registers one or more mutations for the ClusterRoleBinding. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // A mutation with a nil Feature is applied unconditionally; one with a non-nil // Feature is applied only when that feature is enabled. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/configmap/builder.go b/pkg/primitives/configmap/builder.go index 3d706edd..5dd4fe75 100644 --- a/pkg/primitives/configmap/builder.go +++ b/pkg/primitives/configmap/builder.go @@ -40,13 +40,15 @@ func NewBuilder(cm *corev1.ConfigMap) *Builder { } } -// WithMutation registers a mutation for the ConfigMap. +// WithMutation registers one or more mutations for the ConfigMap. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // A mutation with a nil Feature is applied unconditionally; one with a non-nil // Feature is applied only when that feature is enabled. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/cronjob/builder.go b/pkg/primitives/cronjob/builder.go index 22b9ba81..c75c4747 100644 --- a/pkg/primitives/cronjob/builder.go +++ b/pkg/primitives/cronjob/builder.go @@ -49,11 +49,13 @@ func NewBuilder(cj *batchv1.CronJob) *Builder { } } -// WithMutation registers a feature-based mutation for the CronJob. +// WithMutation registers one or more feature-based mutations for the CronJob. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/daemonset/builder.go b/pkg/primitives/daemonset/builder.go index 9f620c8f..09e614ed 100644 --- a/pkg/primitives/daemonset/builder.go +++ b/pkg/primitives/daemonset/builder.go @@ -50,7 +50,7 @@ func NewBuilder(daemonset *appsv1.DaemonSet) *Builder { } } -// WithMutation registers a feature-based mutation for the DaemonSet. +// WithMutation registers one or more feature-based mutations for the DaemonSet. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // They are typically used by Features to inject environment variables, @@ -59,8 +59,10 @@ func NewBuilder(daemonset *appsv1.DaemonSet) *Builder { // Since mutations are often version-gated, the provided feature.Mutation // should contain the logic to determine if and how the mutation is applied // based on the component's current version or configuration. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/deployment/builder.go b/pkg/primitives/deployment/builder.go index 3ebc0028..2a765c05 100644 --- a/pkg/primitives/deployment/builder.go +++ b/pkg/primitives/deployment/builder.go @@ -50,7 +50,7 @@ func NewBuilder(deployment *appsv1.Deployment) *Builder { } } -// WithMutation registers a feature-based mutation for the Deployment. +// WithMutation registers one or more feature-based mutations for the Deployment. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // They are typically used by Features to inject environment variables, @@ -59,8 +59,10 @@ func NewBuilder(deployment *appsv1.Deployment) *Builder { // Since mutations are often version-gated, the provided feature.Mutation // should contain the logic to determine if and how the mutation is applied // based on the component's current version or configuration. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/hpa/builder.go b/pkg/primitives/hpa/builder.go index aab3b88f..41a5d7ff 100644 --- a/pkg/primitives/hpa/builder.go +++ b/pkg/primitives/hpa/builder.go @@ -49,13 +49,15 @@ func NewBuilder(hpa *autoscalingv2.HorizontalPodAutoscaler) *Builder { } } -// WithMutation registers a feature-based mutation for the HPA. +// WithMutation registers one or more feature-based mutations for the HPA. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // A mutation with a nil Feature is applied unconditionally; one with a non-nil // Feature is applied only when that feature is enabled. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/ingress/builder.go b/pkg/primitives/ingress/builder.go index 541d6883..a0d90b8a 100644 --- a/pkg/primitives/ingress/builder.go +++ b/pkg/primitives/ingress/builder.go @@ -49,13 +49,15 @@ func NewBuilder(ing *networkingv1.Ingress) *Builder { } } -// WithMutation registers a mutation for the Ingress. +// WithMutation registers one or more mutations for the Ingress. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // A mutation with a nil Feature is applied unconditionally; one with a non-nil // Feature is applied only when that feature is enabled. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/job/builder.go b/pkg/primitives/job/builder.go index 29a99b8d..54e355b6 100644 --- a/pkg/primitives/job/builder.go +++ b/pkg/primitives/job/builder.go @@ -48,7 +48,7 @@ func NewBuilder(job *batchv1.Job) *Builder { } } -// WithMutation registers a feature-based mutation for the Job. +// WithMutation registers one or more feature-based mutations for the Job. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // They are typically used by Features to inject environment variables, @@ -57,8 +57,10 @@ func NewBuilder(job *batchv1.Job) *Builder { // Since mutations are often version-gated, the provided feature.Mutation // should contain the logic to determine if and how the mutation is applied // based on the component's current version or configuration. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/networkpolicy/builder.go b/pkg/primitives/networkpolicy/builder.go index 14300a79..436fd6bc 100644 --- a/pkg/primitives/networkpolicy/builder.go +++ b/pkg/primitives/networkpolicy/builder.go @@ -41,13 +41,15 @@ func NewBuilder(np *networkingv1.NetworkPolicy) *Builder { } } -// WithMutation registers a mutation for the NetworkPolicy. +// WithMutation registers one or more mutations for the NetworkPolicy. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // A mutation with a nil Feature is applied unconditionally; one with a non-nil // Feature is applied only when that feature is enabled. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/pdb/builder.go b/pkg/primitives/pdb/builder.go index 0d283c48..5b01a00c 100644 --- a/pkg/primitives/pdb/builder.go +++ b/pkg/primitives/pdb/builder.go @@ -40,13 +40,15 @@ func NewBuilder(p *policyv1.PodDisruptionBudget) *Builder { } } -// WithMutation registers a mutation for the PodDisruptionBudget. +// WithMutation registers one or more mutations for the PodDisruptionBudget. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // A mutation with a nil Feature is applied unconditionally; one with a non-nil // Feature is applied only when that feature is enabled. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/pod/builder.go b/pkg/primitives/pod/builder.go index 87b9dea7..97109bee 100644 --- a/pkg/primitives/pod/builder.go +++ b/pkg/primitives/pod/builder.go @@ -50,7 +50,7 @@ func NewBuilder(pod *corev1.Pod) *Builder { } } -// WithMutation registers a feature-based mutation for the Pod. +// WithMutation registers one or more feature-based mutations for the Pod. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // They are typically used by Features to inject environment variables, @@ -59,8 +59,10 @@ func NewBuilder(pod *corev1.Pod) *Builder { // Since mutations are often version-gated, the provided feature.Mutation // should contain the logic to determine if and how the mutation is applied // based on the component's current version or configuration. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/pv/builder.go b/pkg/primitives/pv/builder.go index 2ab2b97d..923be306 100644 --- a/pkg/primitives/pv/builder.go +++ b/pkg/primitives/pv/builder.go @@ -44,14 +44,16 @@ func NewBuilder(pv *corev1.PersistentVolume) *Builder { return &Builder{base: base} } -// WithMutation registers a mutation for the PersistentVolume. +// WithMutation registers one or more mutations for the PersistentVolume. // // Mutations are applied sequentially during the Mutate() phase of reconciliation, // after the baseline field applicator and any registered flavors have run. // A mutation with a nil Feature is applied unconditionally; one with a non-nil // Feature is applied only when that feature is enabled. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/pvc/builder.go b/pkg/primitives/pvc/builder.go index a36ecb00..b0d9b648 100644 --- a/pkg/primitives/pvc/builder.go +++ b/pkg/primitives/pvc/builder.go @@ -48,13 +48,15 @@ func NewBuilder(pvc *corev1.PersistentVolumeClaim) *Builder { } } -// WithMutation registers a mutation for the PVC. +// WithMutation registers one or more mutations for the PVC. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // A mutation with a nil Feature is applied unconditionally; one with a non-nil // Feature is applied only when that feature is enabled. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/replicaset/builder.go b/pkg/primitives/replicaset/builder.go index e0cd24b7..aeb70630 100644 --- a/pkg/primitives/replicaset/builder.go +++ b/pkg/primitives/replicaset/builder.go @@ -50,7 +50,7 @@ func NewBuilder(replicaset *appsv1.ReplicaSet) *Builder { } } -// WithMutation registers a feature-based mutation for the ReplicaSet. +// WithMutation registers one or more feature-based mutations for the ReplicaSet. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // They are typically used by Features to inject environment variables, @@ -59,8 +59,10 @@ func NewBuilder(replicaset *appsv1.ReplicaSet) *Builder { // Since mutations are often version-gated, the provided feature.Mutation // should contain the logic to determine if and how the mutation is applied // based on the component's current version or configuration. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/role/builder.go b/pkg/primitives/role/builder.go index b76c8487..ed92c627 100644 --- a/pkg/primitives/role/builder.go +++ b/pkg/primitives/role/builder.go @@ -40,13 +40,15 @@ func NewBuilder(role *rbacv1.Role) *Builder { } } -// WithMutation registers a mutation for the Role. +// WithMutation registers one or more mutations for the Role. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // A mutation with a nil Feature is applied unconditionally; one with a non-nil // Feature is applied only when that feature is enabled. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/rolebinding/builder.go b/pkg/primitives/rolebinding/builder.go index 0d6a1289..1f045c98 100644 --- a/pkg/primitives/rolebinding/builder.go +++ b/pkg/primitives/rolebinding/builder.go @@ -43,13 +43,15 @@ func NewBuilder(rb *rbacv1.RoleBinding) *Builder { } } -// WithMutation registers a mutation for the RoleBinding. +// WithMutation registers one or more mutations for the RoleBinding. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // A mutation with a nil Feature is applied unconditionally; one with a non-nil // Feature is applied only when that feature is enabled. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/secret/builder.go b/pkg/primitives/secret/builder.go index 4133a628..1ee46e0b 100644 --- a/pkg/primitives/secret/builder.go +++ b/pkg/primitives/secret/builder.go @@ -40,13 +40,15 @@ func NewBuilder(s *corev1.Secret) *Builder { } } -// WithMutation registers a mutation for the Secret. +// WithMutation registers one or more mutations for the Secret. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // A mutation with a nil Feature is applied unconditionally; one with a non-nil // Feature is applied only when that feature is enabled. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/service/builder.go b/pkg/primitives/service/builder.go index f62d513f..bed2a335 100644 --- a/pkg/primitives/service/builder.go +++ b/pkg/primitives/service/builder.go @@ -49,7 +49,7 @@ func NewBuilder(svc *corev1.Service) *Builder { } } -// WithMutation registers a feature-based mutation for the Service. +// WithMutation registers one or more feature-based mutations for the Service. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // They are typically used by Features to modify ports, selectors, or other @@ -58,8 +58,10 @@ func NewBuilder(svc *corev1.Service) *Builder { // Since mutations are often version-gated, the provided feature.Mutation // should contain the logic to determine if and how the mutation is applied // based on the component's current version or configuration. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/serviceaccount/builder.go b/pkg/primitives/serviceaccount/builder.go index abc172b1..05107290 100644 --- a/pkg/primitives/serviceaccount/builder.go +++ b/pkg/primitives/serviceaccount/builder.go @@ -40,13 +40,15 @@ func NewBuilder(sa *corev1.ServiceAccount) *Builder { } } -// WithMutation registers a mutation for the ServiceAccount. +// WithMutation registers one or more mutations for the ServiceAccount. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // A mutation with a nil Feature is applied unconditionally; one with a non-nil // Feature is applied only when that feature is enabled. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/statefulset/builder.go b/pkg/primitives/statefulset/builder.go index 4b3f110b..060de024 100644 --- a/pkg/primitives/statefulset/builder.go +++ b/pkg/primitives/statefulset/builder.go @@ -50,13 +50,15 @@ func NewBuilder(statefulset *appsv1.StatefulSet) *Builder { } } -// WithMutation registers a feature-based mutation for the StatefulSet. +// WithMutation registers one or more feature-based mutations for the StatefulSet. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // They are typically used by Features to inject environment variables, // arguments, or other configuration into the StatefulSet's containers. -func (b *Builder) WithMutation(m Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*Mutator](m)) +func (b *Builder) WithMutation(ms ...Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*Mutator](m)) + } return b } diff --git a/pkg/primitives/statefulset/builder_test.go b/pkg/primitives/statefulset/builder_test.go index b3c96137..d4172ca2 100644 --- a/pkg/primitives/statefulset/builder_test.go +++ b/pkg/primitives/statefulset/builder_test.go @@ -93,6 +93,51 @@ func TestBuilder(t *testing.T) { assert.Equal(t, "test-mutation", res.base.Mutations[0].Name) }) + t.Run("WithMutation variadic registers in order", func(t *testing.T) { + t.Parallel() + sts := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-sts", + Namespace: "test-ns", + }, + } + first := Mutation{Name: "first", Mutate: func(_ *Mutator) error { return nil }} + second := Mutation{Name: "second", Mutate: func(_ *Mutator) error { return nil }} + + builder := NewBuilder(sts) + require.Same(t, builder, builder.WithMutation(first, second)) + + res, err := builder.Build() + require.NoError(t, err) + require.Len(t, res.base.Mutations, 2) + assert.Equal(t, "first", res.base.Mutations[0].Name) + assert.Equal(t, "second", res.base.Mutations[1].Name) + }) + + t.Run("WithMutation spread and zero args", func(t *testing.T) { + t.Parallel() + sts := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-sts", + Namespace: "test-ns", + }, + } + muts := []Mutation{ + {Name: "a", Mutate: func(_ *Mutator) error { return nil }}, + {Name: "b", Mutate: func(_ *Mutator) error { return nil }}, + } + + builder := NewBuilder(sts) + require.Same(t, builder, builder.WithMutation(muts...)) + require.Same(t, builder, builder.WithMutation()) + + res, err := builder.Build() + require.NoError(t, err) + require.Len(t, res.base.Mutations, 2) + assert.Equal(t, "a", res.base.Mutations[0].Name) + assert.Equal(t, "b", res.base.Mutations[1].Name) + }) + t.Run("WithCustomConvergeStatus", func(t *testing.T) { t.Parallel() sts := &appsv1.StatefulSet{ diff --git a/pkg/primitives/unstructured/integration/builder.go b/pkg/primitives/unstructured/integration/builder.go index 0d1de232..b8fe3ab5 100644 --- a/pkg/primitives/unstructured/integration/builder.go +++ b/pkg/primitives/unstructured/integration/builder.go @@ -43,9 +43,11 @@ func (b *Builder) MarkClusterScoped() *Builder { return b } -// WithMutation registers a mutation for the unstructured object. -func (b *Builder) WithMutation(m unstruct.Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*unstruct.Mutator](m)) +// WithMutation registers one or more mutations for the unstructured object. +func (b *Builder) WithMutation(ms ...unstruct.Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*unstruct.Mutator](m)) + } return b } diff --git a/pkg/primitives/unstructured/static/builder.go b/pkg/primitives/unstructured/static/builder.go index 7514a894..8a5a3ef3 100644 --- a/pkg/primitives/unstructured/static/builder.go +++ b/pkg/primitives/unstructured/static/builder.go @@ -48,13 +48,15 @@ func (b *Builder) MarkClusterScoped() *Builder { return b } -// WithMutation registers a mutation for the unstructured object. +// WithMutation registers one or more mutations for the unstructured object. // // Mutations are applied sequentially during the Mutate() phase of reconciliation. // A mutation with a nil Feature is applied unconditionally; one with a non-nil // Feature is applied only when that feature is enabled. -func (b *Builder) WithMutation(m unstruct.Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*unstruct.Mutator](m)) +func (b *Builder) WithMutation(ms ...unstruct.Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*unstruct.Mutator](m)) + } return b } diff --git a/pkg/primitives/unstructured/task/builder.go b/pkg/primitives/unstructured/task/builder.go index cec75889..3336901a 100644 --- a/pkg/primitives/unstructured/task/builder.go +++ b/pkg/primitives/unstructured/task/builder.go @@ -43,9 +43,11 @@ func (b *Builder) MarkClusterScoped() *Builder { return b } -// WithMutation registers a mutation for the unstructured object. -func (b *Builder) WithMutation(m unstruct.Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*unstruct.Mutator](m)) +// WithMutation registers one or more mutations for the unstructured object. +func (b *Builder) WithMutation(ms ...unstruct.Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*unstruct.Mutator](m)) + } return b } diff --git a/pkg/primitives/unstructured/workload/builder.go b/pkg/primitives/unstructured/workload/builder.go index 2b6177a8..6bc4fc65 100644 --- a/pkg/primitives/unstructured/workload/builder.go +++ b/pkg/primitives/unstructured/workload/builder.go @@ -43,9 +43,11 @@ func (b *Builder) MarkClusterScoped() *Builder { return b } -// WithMutation registers a mutation for the unstructured object. -func (b *Builder) WithMutation(m unstruct.Mutation) *Builder { - b.base.WithMutation(feature.Mutation[*unstruct.Mutator](m)) +// WithMutation registers one or more mutations for the unstructured object. +func (b *Builder) WithMutation(ms ...unstruct.Mutation) *Builder { + for _, m := range ms { + b.base.WithMutation(feature.Mutation[*unstruct.Mutator](m)) + } return b }