Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions docs/custom-resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
20 changes: 20 additions & 0 deletions docs/primitives.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions pkg/generic/builder_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
}
Comment thread
sourcehawk marked this conversation as resolved.

// WithGuard registers a guard precondition for the resource.
Expand Down
7 changes: 4 additions & 3 deletions pkg/generic/builder_integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
7 changes: 4 additions & 3 deletions pkg/generic/builder_static.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
50 changes: 50 additions & 0 deletions pkg/generic/builder_static_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
7 changes: 4 additions & 3 deletions pkg/generic/builder_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
7 changes: 4 additions & 3 deletions pkg/generic/builder_workload.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
8 changes: 5 additions & 3 deletions pkg/primitives/clusterrole/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
8 changes: 5 additions & 3 deletions pkg/primitives/clusterrolebinding/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
8 changes: 5 additions & 3 deletions pkg/primitives/configmap/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
8 changes: 5 additions & 3 deletions pkg/primitives/cronjob/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
8 changes: 5 additions & 3 deletions pkg/primitives/daemonset/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
}

Expand Down
8 changes: 5 additions & 3 deletions pkg/primitives/deployment/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
}

Expand Down
8 changes: 5 additions & 3 deletions pkg/primitives/hpa/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
8 changes: 5 additions & 3 deletions pkg/primitives/ingress/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
8 changes: 5 additions & 3 deletions pkg/primitives/job/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
}

Expand Down
Loading
Loading