All posts

The Client That Carries Configuration

4 min read

Every resource in Orkestra applies to Kubernetes via server-side apply. That patch call has one flag that matters more than the others: Force.

When Force is true, the field manager asserts ownership over every field it writes. Conflicts are resolved in its favour. This is what you want for an operator runtime — it should own the resources it manages, and if something else has been touching them, the operator should win.

Force: true had been hardcoded everywhere. All twenty-plus resources, the same value. It was technically correct for the default case and never questioned.

Then the question arrived from two directions at once.


Two callers, one missing knob

The first direction: declarative operators. The operatorBox already lets operator authors control almost everything about the resources they declare — labels, annotations, image, replicas, resource limits. But they couldn’t say “this resource, don’t force-own it.” The field conflict behaviour was invisible to them, hardcoded underneath.

The second direction: library users. pkg/resources is not just the generic reconciler’s implementation detail. It is usable as a Go library — someone building a constructor can use Orkestra’s resource types instead of writing their own deployment and service structs. But those callers inherit Force: true without asking for it. If they want standard Kubernetes apply semantics, they have to reimplement the patch call from scratch.

Neither should be true. The resolution hierarchy was obvious once the problem was named:

resource-level setting     (most specific — declared per resource)
      │ if not set
CRD-level setting          (fallback — declared once for all resources)
      │ if not set
system default: true       (the runtime owns what it manages)

The challenge was how to get the CRD-level setting down to the resources package without creating a dependency that didn’t belong.


The client was already doing this

pkg/resources does not know about CRD entries. It should not know about CRD entries. It knows about resource specs and a kube client and how to apply them. That separation is deliberate.

But the kube client was already carrying things downward. Informers travel this way. Event recorders travel this way. Args — the user-facing configuration from hooks.args and constructor.args — already travel this way. The kubeclient.Interface was already a downward channel for context that reconcilers needed but that the resources package shouldn’t have to source independently.

The CRD-level forceConflict fit the same pattern.

Two methods on the interface:

WithForceConflict(forceConflict *bool) Interface
ForceConflict() *bool

At construction time, the runtime sets the CRD-level value:

kube.WithForceConflict(crd.ResolveForceConflict())

Inside pkg/resources/common, the resolution is ten lines that don’t know where the CRD-level setting came from:

func ResolveForceConflict(kube kubeclient.Interface, resourceForceConflict *bool) *bool {
    if resourceForceConflict != nil {
        return resourceForceConflict
    }
    if kube != nil {
        if fc := kube.ForceConflict(); fc != nil {
            return fc
        }
    }
    defaultForceConflict := true
    return &defaultForceConflict
}

The resource asks: was I configured? No — ask the client. Was the client configured? No — use the system default.

Three layers of resolution in one function. Each layer ignorant of the layers above it. Correct about its own concern.


What this opens

Every resource now calls this function at apply time. The result flows through to the Kubernetes patch options. Nothing else changes.

The generic reconciler’s YAML authors can now write:

deployments:
  - name: "{{ .metadata.name }}"
    forceConflict: false

And get standard Kubernetes apply semantics for that specific deployment, while other resources in the same operatorBox continue to use the runtime’s default ownership behaviour.

A constructor author who calls pkg/resources directly passes their kube client — already configured with WithForceConflict at construction time — and gets the right behaviour without writing a single patch call themselves. The library they are using becomes configurable through the same mechanism as everything else in Orkestra.

This is the library goal stated precisely: do it once in the resources package, use it correctly from the generic reconciler, from hooks, from constructors, from external Go callers. The kube client is the channel that connects all of them without any of them knowing about the others.


The pattern

forceConflict is the second non-infrastructure configuration to travel through kubeclient.Interface. Args was the first.

Both follow the same shape: the configuration originates in the user-facing YAML, gets resolved at construction time by the runtime, gets attached to the kube client with a With* method, and becomes available to any caller that receives the client — without that caller needing to know where the configuration came from.

When the next piece of CRD-level configuration needs to travel this path, the pattern is already there.