Server-Side Apply Plugin (ssa/v1-alpha)
The ssa plugin scaffolds APIs with controllers that use Server-Side Apply, enabling safer field management when resources are shared between your controller and users or other controllers.
By using this plugin, you will get:
- A controller implementation using Server-Side Apply patterns
- Automatic generation of apply configuration types for type-safe Server-Side Apply
- Makefile integration to generate apply configurations alongside DeepCopy methods
- Tests using the apply configuration patterns
When to use it?
Use this plugin when:
- Multiple controllers manage the same resource: Your controller manages some fields while other controllers or users manage others
- Users customize your CRs: Users add their own labels, annotations, or spec fields that your controller shouldn’t overwrite
- Partial field management: You only want to manage specific fields and leave others alone
- Avoiding conflicts: You want declarative field ownership tracking to prevent accidental overwrites
Don’t use it when:
- Your controller is the sole owner of the resource (traditional Update/Patch is simpler)
- You manage the entire object (no shared ownership)
- Simple CRUD operations where you control everything
How does it work?
Traditional Update vs Server-Side Apply
Traditional approach (without this plugin):
func (r *MyResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var resource myv1.MyResource
if err := r.Get(ctx, req.NamespacedName, &resource); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Problem: This overwrites ALL fields, including user customizations
resource.Spec.Replicas = 3
resource.Labels["managed-by"] = "my-controller"
if err := r.Update(ctx, &resource); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
Server-Side Apply approach (with this plugin):
import (
myv1 "example.com/project/api/apps/v1"
myv1apply "example.com/project/applyconfiguration/apps/v1"
)
func (r *MyResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// Fetch the resource
resource := &myv1.MyResource{}
if err := r.Get(ctx, req.NamespacedName, resource); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Build desired state - only specify fields you want to manage
resourceApply := myv1apply.MyResource(req.Name, req.Namespace).
WithSpec(myv1apply.MyResourceSpec().
WithReplicas(3)).
WithLabels(map[string]string{
"managed-by": "my-controller",
})
// Apply - only manages the fields you specified above
// User's custom labels/annotations are preserved!
if err := r.Patch(ctx, resource, client.Apply, client.ForceOwnership,
client.FieldOwner("my-controller")); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
What gets generated
-
API types with applyconfiguration markers in
api/<group>/<version>/:+kubebuilder:ac:generate=trueingroupversion_info.go(package-level)+genclientmarker on your CRD type in*_types.go
-
Minimal controller template ready for Server-Side Apply:
func (r *MyKindReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { // TODO(user): Update the MyKind status using Server-Side Apply // Uncomment and customize the code below after running 'make generate' // // statusApply := mykindv1applyconfig.MyKind(myKind.Name, myKind.Namespace). // WithStatus(mykindv1applyconfig.MyKindStatus()) // // if err := r.Status().Patch(ctx, statusApply, client.Apply, // client.ForceOwnership, client.FieldOwner("mykind-controller")); err != nil { // return ctrl.Result{}, err // } return ctrl.Result{}, nil } -
Updated Makefile (first API only):
.PHONY: generate generate: controller-gen "$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" \ applyconfiguration:headerFile="hack/boilerplate.go.txt" paths="./..."The
applyconfigurationgeneration is added to the existing object generation command. -
Apply configuration types (generated by
make generate):applyconfiguration/ └── <group>/<version>/ ├── mykind.go ├── mykindspec.go └── mykindstatus.go
How to use it?
1. Initialize your project
kubebuilder init --domain example.com --repo example.com/myproject
2. Create API with the plugin
kubebuilder create api \
--group apps \
--version v1 \
--kind Application \
--plugins="ssa/v1-alpha"
3. Customize your controller
The scaffolded controller includes a TODO where you specify which fields to manage:
// TODO(user): Build desired state using apply configuration
resourceApply := appsv1apply.Application(req.Name, req.Namespace).
WithSpec(appsv1apply.ApplicationSpec())
// Add your desired fields here
4. Generate and run
make manifests generate
make test
make run
Mixing with traditional APIs
You can use this plugin for specific APIs only. Other APIs in the same project can use traditional Update:
# API A - traditional approach (no plugin)
kubebuilder create api --group core --version v1 --kind Config
# API B - with Server-Side Apply plugin
kubebuilder create api --group apps --version v1 --kind Workload \
--plugins="ssa/v1-alpha"
# API C - traditional approach (no plugin)
kubebuilder create api --group core --version v1 --kind Status
Result:
ConfigandStatuscontrollers use traditional UpdateWorkloadcontroller uses Server-Side Apply- Only
Workloadhas apply configurations generated - All make targets (
build-installer,test, etc.) work unchanged
Subcommands
The server-side-apply plugin includes the following subcommand:
create api: Scaffolds the API with a controller using Server-Side Apply patterns
Affected files
When using the create api command with this plugin, the following
files are affected:
api/<group>/<version>/*_types.go: Scaffolds the API types (same as standard)internal/controller/*_controller.go: Scaffolds controller using Server-Side Applyconfig/crd/bases/*: Scaffolds CRD (same as standard)config/samples/*: Scaffolds sample CR (same as standard)Makefile: Adds apply configuration generation for this API.gitignore: Addspkg/applyconfiguration/(first time only)cmd/main.go: Registers the controller (same as standard)
Generated file structure
After creating an API with this plugin:
api/
└── apps/v1/
├── application_types.go
└── zz_generated.deepcopy.go
internal/controller/
└── application_controller.go # Uses Server-Side Apply
pkg/applyconfiguration/ # Generated by 'make generate'
└── apps/v1/
├── application.go
├── applicationspec.go
└── applicationstatus.go
Makefile # Updated with applyconfiguration target
.gitignore # Excludes pkg/applyconfiguration/
Best practices
1. Always specify FieldOwner
client.FieldOwner("my-controller-name")
This identifies your controller in the managed fields.
2. Use ForceOwnership carefully
client.ForceOwnership // Takes ownership even if another manager owns the field
Only use when you’re certain your controller should own all specified fields.
3. Only manage what you need
// Good - only manage replicas
appApply := appsv1apply.Application(name, ns).
WithSpec(appsv1apply.ApplicationSpec().
WithReplicas(3))
// Avoid - managing entire spec might conflict with users
appApply := appsv1apply.Application(name, ns).
WithSpec(spec) // Don't set the entire spec
4. Handle conflicts
err := r.Patch(ctx, obj, client.Apply, client.FieldOwner("my-controller"))
if err != nil {
if errors.IsConflict(err) {
// Conflict detected - another manager owns the same field
// Decide: retry, log, or force ownership
}
}
Additional resources
For more details on Server-Side Apply concepts and patterns, see:
- Server-Side Apply Reference - Concepts and theory
- Kubernetes Server-Side Apply Documentation
- controller-gen CLI Reference
Plugin Compatibility
Cannot be used with deploy-image plugin:
The server-side-apply and deploy-image plugins scaffold different controller implementations and cannot be used together for the same API.
# This will fail:
kubebuilder create api --group apps --version v1 --kind App \
--image=nginx:latest \
--plugins=deploy-image/v1-alpha,ssa/v1-alpha
# Instead, choose one plugin per API:
kubebuilder create api --group apps --version v1 --kind App \
--plugins=ssa/v1-alpha # OR deploy-image/v1-alpha
Example in Testdata
To see examples of SSA API scaffolds, check the testdata samples:
Single-group project: testdata/project-v4-with-plugins
api/v1/application_types.go- API with+genclientmarkerapi/v1/groupversion_info.go- Package with+kubebuilder:ac:generate=trueinternal/controller/application_controller.go- Minimal SSA controller template
Multi-group project: testdata/project-v4-multigroup
api/sea-creatures/v1/prawn_types.go- API with+genclientmarkerapi/sea-creatures/v1/groupversion_info.go- Package with+kubebuilder:ac:generate=trueinternal/controller/sea-creatures/prawn_controller.go- Minimal SSA controller template
Memcached Operator Example with SSA
For a complete example showing how to build a Memcached operator using Server-Side Apply,
see the Getting Started with SSA (docs/book/src/getting-started-ssa/testdata/project).
This tutorial demonstrates:
- Building a full operator with SSA for managing Deployments
- Implementing reconciliation logic with apply configurations
- Status management and condition handling
- Complete end-to-end testing