Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. API types with applyconfiguration markers in api/<group>/<version>/:

    • +kubebuilder:ac:generate=true in groupversion_info.go (package-level)
    • +genclient marker on your CRD type in *_types.go
  2. 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
    }
    
  3. 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 applyconfiguration generation is added to the existing object generation command.

  4. 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:

  • Config and Status controllers use traditional Update
  • Workload controller uses Server-Side Apply
  • Only Workload has 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 Apply
  • config/crd/bases/*: Scaffolds CRD (same as standard)
  • config/samples/*: Scaffolds sample CR (same as standard)
  • Makefile: Adds apply configuration generation for this API
  • .gitignore: Adds pkg/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:

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

Multi-group project: testdata/project-v4-multigroup

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