Using ApplyConfigurations for Server-Side Apply
Cluster API provides generated ApplyConfiguration types that enable type-safe Server-Side Apply (SSA) operations in Go. This allows multiple controllers and operators to cooperatively manage Cluster API resources without conflicts.
Overview
ApplyConfigurations provide a builder pattern for constructing partial resource definitions that can be applied using Kubernetes Server-Side Apply. Unlike traditional Update operations, SSA allows multiple actors (controllers, operators, users) to manage different fields of the same resource independently.
Benefits of Server-Side Apply
- Field Management: Kubernetes tracks which controller owns which fields
- Conflict Resolution: Automatic conflict detection and resolution
- Cooperative Management: Multiple controllers can safely update different fields
- Atomic Operations: Changes are applied atomically
Generated Packages
The generated code is located in:
- ApplyConfigurations:
pkg/generated/applyconfiguration/ - Typed Clients:
pkg/generated/client/
Supported API groups:
core/v1beta2- Cluster, Machine, MachineDeployment, MachineSet, MachinePool, ClusterClass, MachineHealthCheckaddons/v1beta2- ClusterResourceSet, ClusterResourceSetBindingbootstrap/kubeadm/v1beta2- KubeadmConfigcontrolplane/kubeadm/v1beta2- KubeadmControlPlane
Usage Examples
Creating a Cluster with SSA
package main
import (
"context"
applycorev1beta2 "sigs.k8s.io/cluster-api/pkg/generated/applyconfiguration/core/v1beta2"
"sigs.k8s.io/controller-runtime/pkg/client"
)
func createCluster(ctx context.Context, c client.Client) error {
// Build the Cluster apply configuration
clusterApply := applycorev1beta2.Cluster("my-cluster", "default").
WithSpec(applycorev1beta2.ClusterSpec().
WithControlPlaneEndpoint(applycorev1beta2.APIEndpoint().
WithHost("10.0.0.1").
WithPort(6443)).
WithClusterNetwork(applycorev1beta2.ClusterNetwork().
WithServiceDomain("cluster.local")))
// Apply the configuration
err := c.Apply(ctx, clusterApply,
client.FieldOwner("my-controller"),
client.ForceOwnership)
return err
}
Updating Machine Deployment Replicas
func scaleDeployment(ctx context.Context, c client.Client, name, namespace string, replicas int32) error {
// Build a partial apply configuration with just the replica count
mdApply := applycorev1beta2.MachineDeployment(name, namespace).
WithSpec(applycorev1beta2.MachineDeploymentSpec().
WithReplicas(replicas))
// Apply only the replica field
return c.Apply(ctx, mdApply,
client.FieldOwner("autoscaler"),
client.ForceOwnership)
}
Setting Infrastructure Reference
func setInfrastructureRef(ctx context.Context, c client.Client, clusterName, namespace string) error {
clusterApply := applycorev1beta2.Cluster(clusterName, namespace).
WithSpec(applycorev1beta2.ClusterSpec().
WithInfrastructureRef(
&corev1.ObjectReference{
APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
Kind: "MyInfraCluster",
Name: clusterName + "-infra",
Namespace: namespace,
},
))
return c.Apply(ctx, clusterApply,
client.FieldOwner("infrastructure-controller"))
}
Working with Status Subresources
func updateClusterStatus(ctx context.Context, c client.Client, clusterName, namespace string) error {
statusApply := applycorev1beta2.Cluster(clusterName, namespace).
WithStatus(applycorev1beta2.ClusterStatus().
WithPhase("Provisioned").
WithConditions(metav1.Condition{
Type: "Ready",
Status: metav1.ConditionTrue,
Reason: "ClusterReady",
}))
// Use Status().Apply() for status subresource
return c.Status().Apply(ctx, statusApply,
client.FieldOwner("cluster-controller"))
}
Field Ownership
Each controller should use a unique FieldOwner identifier when calling Apply:
client.FieldOwner("cluster-topology-controller")
client.FieldOwner("machine-controller")
client.FieldOwner("my-custom-operator")
This allows Kubernetes to track which controller manages which fields and detect conflicts.
Force Ownership
Use client.ForceOwnership to take over fields from other controllers:
err := c.Apply(ctx, clusterApply,
client.FieldOwner("priority-controller"),
client.ForceOwnership) // Take over conflicting fields
Warning: Use ForceOwnership carefully as it can overwrite changes made by other controllers.
Using the Typed Client
The generated typed client provides direct access to Apply methods:
import (
"k8s.io/client-go/rest"
clientset "sigs.k8s.io/cluster-api/pkg/generated/client/clientset"
)
func example(config *rest.Config) error {
// Create the clientset
cs, err := clientset.NewForConfig(config)
if err != nil {
return err
}
// Use the typed client for core/v1beta2 resources
clusterClient := cs.ClusterV1beta2().Clusters("default")
// Apply configuration
result, err := clusterClient.Apply(ctx, clusterApply, metav1.ApplyOptions{
FieldManager: "my-controller",
})
return err
}
Comparison with controller-runtime
Using controller-runtime (existing approach)
// Convert to Unstructured at runtime
modifiedUnstructured, err := prepareModified(c.Scheme(), modified)
if err != nil {
return err
}
err = c.Apply(ctx, client.ApplyConfigurationFromUnstructured(modifiedUnstructured),
client.FieldOwner("my-controller"))
Using ApplyConfigurations (new approach)
// Type-safe builder pattern
clusterApply := applycorev1beta2.Cluster("my-cluster", "default").
WithSpec(applycorev1beta2.ClusterSpec().
WithControlPlaneEndpoint(...))
err := c.Apply(ctx, clusterApply,
client.FieldOwner("my-controller"))
Best Practices
- Use Specific Field Owners: Choose descriptive, unique field owner names
- Minimize Force Ownership: Only use when absolutely necessary
- Apply Partial Updates: Only include fields you want to manage
- Handle Conflicts: Check for and handle ownership conflicts gracefully
- Document Field Ownership: Clearly document which controller owns which fields
Limitations
- Only available for v1beta2 APIs
- Requires controller-runtime v0.18.0 or later
- Not all nested types may have apply configurations generated
Troubleshooting
Conflict Errors
If you encounter conflicts:
Apply failed: field managed by another controller
Solutions:
- Use a different field or coordinate with the other controller
- Use
ForceOwnershipif your controller should take precedence - Check the field manager using
kubectl get <resource> -o yamland look formanagedFields
Missing Apply Methods
If apply methods are missing for a type:
- Verify the type has
+genclienttags in its source - Run
make generateto regenerate code - Check that the type is in a supported API group