kubewright

package module
v0.1.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 29, 2026 License: Apache-2.0 Imports: 0 Imported by: 0

README

kubewright, Kubernetes building blocks for Go

The child-resource code every operator writes, written once.

CI Version Go Reference Kubernetes 1.37 License

probes, err := presets.ExpandProbes(&types.ProbesConfig{
	Readiness: &types.ProbeConfig{Path: "/healthz", Preset: "fast"},
}, presets.Builtin())

res, err := deployments.Apply(ctx, clientset, app, deployments.Spec{
	Name:     "web",
	Image:    "nginx:1.27",
	Replicas: 2,
	Port:     8080,
	Probes:   probes,
}, types.Options{FieldManager: "my-operator"})
// res == types.Applied; the Deployment is owned by app and goes when app goes

Build and write Kubernetes resources for an owner from plain Go specs, with idempotent verbs and named presets for the settings every team repeats.

kubewright is a library, not a framework. There is no controller, no reconcile loop and no logging. It turns a spec into an object, sets its owner reference, writes it with the call you choose, and tells you what happened. Every verb is idempotent: call it again and you get Unchanged or NotFound, not an error. Your controller, CLI or test decides when to call it.

Why

Every operator writes the same code for its child resources. Build a Deployment from a few fields. Set the owner reference so it is garbage collected. Create it if it is missing, and don't fail if it is already there. Write it with server-side apply. Delete it, but only if this owner made it. Copy a Secret into the namespaces that need it.

None of that is specific to one operator, and getting it right takes care: a selector that matches the pod template, a field manager on every apply, a not-found that is an outcome rather than an error. And the Kubernetes API hardly moves: a Deployment, a Service and an owner reference have the same shape in every cluster, and server-side apply works the same everywhere. So that code can be written once, tested once and used everywhere: in a controller, a CLI or a test. kubewright is that code, with the same verbs and results for each kind.

Every team also repeats the same settings: requests and limits sized small, medium or large, probe timings for a fast API or a slow-starting JVM, a restricted security context, a rollout that never drops capacity. Presets give those settings names. You write medium, restricted or safe, and expanding the preset fills in the fields. The built-ins cover the common cases, and your own presets add to them or replace them.

Install

go get github.com/intentd/kubewright

A complete program is in examples/main.go: an App that owns a Deployment and a Service, with presets expanded, each verb's result printed, and a delete that leaves another owner's objects alone. It runs against client-go's fake clientset, so it needs no cluster: go run ./examples.

The verbs

Each kind is a package under core, and each package has the same verbs:

Verb Does Results
Build(owner, spec) returns the object, with the owner reference set. No I/O.
Create(ctx, kube, owner, spec, opts) creates the object if it does not exist Created, Unchanged
Apply(ctx, kube, owner, spec, opts) writes it with server-side apply Applied, Unchanged, Replaced
Delete(ctx, kube, owner, spec) deletes it Deleted, NotFound
DeleteIfOwned(ctx, kube, name, namespace, owned) deletes it only if owned says it is yours Deleted, NotFound, NotOwned

For a cluster-scoped kind, DeleteIfOwned takes no namespace: DeleteIfOwned(ctx, kube, name, owned).

opts is a types.Options: the field manager the write is recorded under and, for Apply, whether to take over fields another manager owns. Create and Apply take the same one, so every object says who wrote it.

Create makes an object that is missing and never changes one that exists: an existing object is Unchanged even when it differs from the spec. To bring an existing object in line with the spec, use Apply.

kube is a kubernetes.Interface, so a real clientset and fake.NewClientset() both work. The owner is any object with metadata, such as your custom resource, a typed object or an unstructured.Unstructured. A spec with no namespace uses the owner's.

Every expected outcome comes back as a types.Result with a nil error. Calling a verb twice is safe: an object that already exists is Unchanged, and one already gone is NotFound. An error means the call failed, such as an invalid spec or a refused request. The library does not log. Log the result if you want to:

res, err := services.Create(ctx, clientset, app, spec, types.Options{FieldManager: "my-operator"})
if err != nil {
	return err
}
log.Printf("service %s: %s", spec.Name, res) // "service web: created"

Kinds

Each built-in kind is a package under core, named for the kind in plural: core/deployments, core/secrets, core/clusterroles.

Every kind has all five verbs, except these:

Kind Differs
serviceaccounts no Apply: nothing in a ServiceAccount drifts after it is created
jobs no Apply: a Job's pod template can't change after it is created. No DeleteIfOwned
customresources no Delete; delete with DeleteIfOwned

customresources covers any other kind, your own custom resources included, through a dynamic.Interface and a meta.RESTMapper.

A few kinds have one more function, for a job their verbs don't cover:

  • CopyToNamespaces, in secrets, configmaps, limitranges, networkpolicies and resourcequotas, copies one object into each of a list of namespaces. A namespace that already has it is skipped. Copies are owned by the owner when it can own them (see Owners); label them to clean up after a namespaced owner.
  • jobs.CreateForDelete creates a Job with no owner reference, for cleanup work that runs while the owner is being deleted and must outlive it.
err := secrets.CopyToNamespaces(ctx, clientset, app, secrets.Spec{
	Name:          "db-credentials",
	FromSecret:    "master-db-creds",
	FromNamespace: "platform",
}, []string{"team", "monitoring", "staging"}, types.Options{FieldManager: "my-operator"})

Pods with more than one container

The pod kinds (deployments, statefulsets, replicasets, pods, jobs, cronjobs) describe one container with their flat fields: Image, Port, Env, Probes and the rest. For more, list them in Containers, and add InitContainers to run first, in order:

deployments.Spec{
	Name: "web",
	Env:  []types.EnvVar{{Name: "REGION", Value: "eu"}}, // shared by every container
	Containers: []types.Container{
		{Name: "app", Image: "app:1.4", Port: 8080},
		{Name: "proxy", Image: "envoyproxy/envoy:v1.31", Port: 9901},
	},
	InitContainers: []types.Container{
		{Name: "migrate", Image: "app:1.4", Command: []string{"migrate", "up"}},
	},
}

With Containers set, the flat fields split in two:

Flat fields With Containers
Env, EnvFrom, SecurityContext, VolumeMounts defaults every container and init container inherits; a container's own value wins (Env by name, VolumeMounts by mount path)
Image, Command, Args, Port, Probes, Resources an error: they belong to one container, so set them on it

Pod-level fields (ImagePullSecrets, Volumes, ServiceAccountName, NodeSelector, PodSecurity, and RestartPolicy on pods, jobs and cronjobs) stay on the spec either way. A pod takes Always, OnFailure or Never; a job or cronjob takes OnFailure, the default, or Never. Init containers can't have probes, and names are unique across the pod. A StatefulSet mounts its volume claim templates on the first container.

Owners

Build sets one controller owner reference to the owner, so Kubernetes deletes the object when the owner is deleted, wherever Kubernetes honours one: a cluster-scoped owner can own anything, a namespaced owner only objects in its own namespace. An object in another namespace, or a cluster-scoped kind (namespaces, pvs, clusterroles, clusterrolebindings, a cluster-scoped custom resource) with a namespaced owner, gets no reference, because the garbage collector would treat it as orphaned and delete it at once. Label those objects and delete them with OwnedByLabels. types.CanOwn(owner, namespace) tells you which case you're in, and types.OwnerReferences(owner, namespace) is the reference Build sets.

DeleteIfOwned takes a predicate rather than an owner, so you decide what ownership means. Three come with the library:

Predicate Owned when the object Use it for
types.OwnedByReference(owner) has an owner reference to owner anything Build set a reference on
types.OwnedByLabels(labels) carries every one of these labels objects that can't hold a reference, such as a cluster-scoped object with a namespaced owner
types.OwnedByAnnotations(annotations) carries every one of these annotations the same, when the marker shouldn't be selectable or is too long for a label

An empty set of labels or annotations matches nothing, so a missing value never deletes by accident.

res, err := configmaps.DeleteIfOwned(ctx, clientset, "web-config", "team-namespace", types.OwnedByReference(app))
// NotOwned if another owner made it: it is left alone

For a cluster-scoped object with a namespaced owner, mark it when you create it, and delete by the same mark:

mine := map[string]string{"app.example.com/owner": "team.shop"}

clusterroles.Create(ctx, clientset, app, clusterroles.Spec{Name: "shop-reader", Labels: mine, Rules: rules}, opts)
res, err := clusterroles.DeleteIfOwned(ctx, clientset, "shop-reader", types.OwnedByLabels(mine))

Server-side apply

Every Apply writes through one function, apply.Typed, so all kinds apply the same way. types.Options sets the field manager and whether to take fields another manager owns:

opts := types.Options{FieldManager: "my-operator", Force: true}

An empty field manager means kubewright. Without Force, a field another manager owns is a conflict, returned as an error.

apply.Object writes any object, typed or unstructured, through the dynamic client. Use it for objects you build yourself:

d := deployments.Build(app, spec)
d.TypeMeta = metav1.TypeMeta{APIVersion: "apps/v1", Kind: "Deployment"}
res, err := apply.Object(ctx, dyn, mapper, d, opts)

Presets

Some fields are the same settings repeated: resource requests and limits, probe timings, security contexts. A field can name a preset instead of spelling them out:

Presets for Built in
Resources tiny, small, medium, large, burst, steady, compute-heavy, memory-heavy
Probes fast, standard, patient, slow-start
Container security baseline, restricted, hardened
Pod security baseline, restricted, hardened
Rolling update safe, fast, blue-green
HPA web, api, latency-sensitive, batch, cost-optimized
PDB zero-downtime, rolling, relaxed
Network policy deny-all, deny-all-ingress, deny-all-egress, allow-same-namespace, allow-dns-egress
Resource quota small, medium, large, xlarge

presets.Builtin() returns them as data. Add your own, or replace a built-in, with With. Names ignore case:

reg := presets.Builtin().With(presets.Registry{
	Resources: []presets.Resource{{
		Name:     "myorg-gpu",
		Requests: map[string]string{"cpu": "4", "memory": "16Gi", "nvidia.com/gpu": "1"},
	}},
})

r, err := presets.ExpandResources(&types.ResourceRequirements{Preset: "myorg-gpu"}, reg)

Expand a preset into explicit fields before you build. There is one Expand function per kind of preset, and each returns an error for an unknown name, so a typo doesn't quietly give you no limits:

spec.RollingUpdate, err = presets.ExpandRollingUpdate(&types.RollingUpdateBehavior{Preset: "safe"}, reg)
spec.Hard, err = presets.ExpandResourceQuota("small", nil, reg)

Each one keeps the rule that fits its field. A PDB or limit range preset fills only what you left empty. A network policy or resource quota preset replaces the rules. An HPA preset sets the scaling behavior, and the CPU target too when you haven't set one.

A container's presets (resources, security context, probes) expand in one call. Call it for a pod kind's flat fields and for each of its containers and init containers:

c, err := presets.ExpandContainer(types.Container{
	Name:      "app",
	Image:     "app:1.4",
	Resources: &types.ResourceRequirements{Preset: "small"},
	Probes:    &types.ProbesConfig{Readiness: &types.ProbeConfig{Path: "/healthz", Preset: "fast"}},
}, reg)

A probe preset fills the timings the probe doesn't set itself, so a probe can name a preset and still override any of its timings, down to 0. A probe that names no preset gets only the timings it sets, and Kubernetes defaults the rest. Build never looks a preset up: Create and Apply refuse a container that still names one, so a preset you forgot to expand is an error, not a silent default.

Testing

Because every verb takes a kubernetes.Interface, fake.NewClientset() from client-go runs them in a unit test, server-side apply included:

cs := fake.NewClientset()
res, _ := deployments.Create(ctx, cs, app, spec, opts) // Created
res, _ = deployments.Create(ctx, cs, app, spec, opts)  // Unchanged

Reading what you built

kubewright writes objects and tells you what the write did. To ask how they are doing afterwards, such as whether a rollout finished or a pod is crash looping, see KEL, its companion library for reading Kubernetes state.

Status

kubewright is before 1.0. Names and signatures may still change between minor versions. It started inside Inrun, which builds every child resource with it, and is now a separate module with no dependency on Inrun.

Contributing

A good change:

  • keeps the verbs the same across kinds. A new kind has Build, Create, Apply, Delete and DeleteIfOwned unless the API rules one out
  • keeps Build pure and every expected outcome a Result, not an error
  • writes through apply.Typed or apply.Object, not its own patch
  • adds a test with TestBuild and TestLifecycle, as the existing kinds do

go test ./... runs every kind against the fake clientset.

License

Apache 2.0

Documentation

Overview

Package kubewright builds and writes Kubernetes resources for an owner object: from a plain Go spec to a Deployment, Service, Secret or another built-in kind, with owner references set so the cluster removes them with their owner. It is a library, not a framework: no controller, no reconcile loop, no logging. You call it from your own.

Each kind is its own package under core, with the same verbs:

  • Build returns the object for a spec and does no I/O.
  • Create creates it if it does not exist.
  • Apply writes it with server-side apply.
  • Delete deletes it; DeleteIfOwned deletes it only if a predicate says the caller owns it.

Every verb takes a kubernetes.Interface (customresources takes a dynamic.Interface and a RESTMapper) and returns a types.Result, such as Created, Unchanged or NotOwned, with a nil error for every expected outcome. Calling a verb twice is safe: an object that already exists is Unchanged, and one already gone is NotFound.

The packages:

  • core/<kind>: one package per kind, such as core/deployments
  • core/types: the field types specs are made of, Owner, Options and Result
  • core/apply: the one server-side apply every Apply writes through
  • presets: named field settings ("medium", "restricted", "safe") that a spec refers to instead of spelling them out

Directories

Path Synopsis
core
apply
Package apply writes objects with server-side apply.
Package apply writes objects with server-side apply.
clusterrolebindings
Package clusterrolebindings builds and writes ClusterRoleBindings with Build, Create, Apply, Delete and DeleteIfOwned.
Package clusterrolebindings builds and writes ClusterRoleBindings with Build, Create, Apply, Delete and DeleteIfOwned.
clusterroles
Package clusterroles builds and writes ClusterRoles with Build, Create, Apply, Delete and DeleteIfOwned.
Package clusterroles builds and writes ClusterRoles with Build, Create, Apply, Delete and DeleteIfOwned.
configmaps
Package configmaps builds and writes ConfigMaps with Build, Create, Apply, Delete and DeleteIfOwned.
Package configmaps builds and writes ConfigMaps with Build, Create, Apply, Delete and DeleteIfOwned.
cronjobs
Package cronjobs builds and writes CronJobs with Build, Create, Apply, Delete and DeleteIfOwned.
Package cronjobs builds and writes CronJobs with Build, Create, Apply, Delete and DeleteIfOwned.
customresources
Package customresources builds and writes custom resources, or any kind without a typed package, with the same verbs as the typed packages: Build, Create, Apply and DeleteIfOwned.
Package customresources builds and writes custom resources, or any kind without a typed package, with the same verbs as the typed packages: Build, Create, Apply and DeleteIfOwned.
deployments
Package deployments builds and writes Deployments with Build, Create, Apply, Delete and DeleteIfOwned.
Package deployments builds and writes Deployments with Build, Create, Apply, Delete and DeleteIfOwned.
hpas
Package hpas builds and writes HorizontalPodAutoscalers with Build, Create, Apply, Delete and DeleteIfOwned.
Package hpas builds and writes HorizontalPodAutoscalers with Build, Create, Apply, Delete and DeleteIfOwned.
ingresses
Package ingresses builds and writes Ingresses with Build, Create, Apply, Delete and DeleteIfOwned.
Package ingresses builds and writes Ingresses with Build, Create, Apply, Delete and DeleteIfOwned.
jobs
Package jobs builds and writes Jobs with Build, Create, Delete and CreateForDelete.
Package jobs builds and writes Jobs with Build, Create, Delete and CreateForDelete.
limitranges
Package limitranges builds and writes LimitRanges with Build, Create, Apply, Delete and DeleteIfOwned.
Package limitranges builds and writes LimitRanges with Build, Create, Apply, Delete and DeleteIfOwned.
namespaces
Package namespaces builds and writes Namespaces with Build, Create, Apply, Delete and DeleteIfOwned.
Package namespaces builds and writes Namespaces with Build, Create, Apply, Delete and DeleteIfOwned.
networkpolicies
Package networkpolicies builds and writes NetworkPolicies with Build, Create, Apply, Delete and DeleteIfOwned.
Package networkpolicies builds and writes NetworkPolicies with Build, Create, Apply, Delete and DeleteIfOwned.
pdbs
Package pdbs builds and writes PodDisruptionBudgets with Build, Create, Apply, Delete and DeleteIfOwned.
Package pdbs builds and writes PodDisruptionBudgets with Build, Create, Apply, Delete and DeleteIfOwned.
pods
Package pods builds and writes Pods with Build, Create, Apply, Delete and DeleteIfOwned.
Package pods builds and writes Pods with Build, Create, Apply, Delete and DeleteIfOwned.
pvcs
Package pvcs builds and writes PersistentVolumeClaims with Build, Create, Apply, Delete and DeleteIfOwned.
Package pvcs builds and writes PersistentVolumeClaims with Build, Create, Apply, Delete and DeleteIfOwned.
pvs
Package pvs builds and writes PersistentVolumes with Build, Create, Apply, Delete and DeleteIfOwned.
Package pvs builds and writes PersistentVolumes with Build, Create, Apply, Delete and DeleteIfOwned.
replicasets
Package replicasets builds and writes ReplicaSets with Build, Create, Apply, Delete and DeleteIfOwned.
Package replicasets builds and writes ReplicaSets with Build, Create, Apply, Delete and DeleteIfOwned.
resourcequotas
Package resourcequotas builds and writes ResourceQuotas with Build, Create, Apply, Delete and DeleteIfOwned.
Package resourcequotas builds and writes ResourceQuotas with Build, Create, Apply, Delete and DeleteIfOwned.
rolebindings
Package rolebindings builds and writes RoleBindings with Build, Create, Apply, Delete and DeleteIfOwned.
Package rolebindings builds and writes RoleBindings with Build, Create, Apply, Delete and DeleteIfOwned.
roles
Package roles builds and writes Roles with Build, Create, Apply, Delete and DeleteIfOwned.
Package roles builds and writes Roles with Build, Create, Apply, Delete and DeleteIfOwned.
secrets
Package secrets builds and writes Secrets with Build, Create, Apply, Delete and DeleteIfOwned.
Package secrets builds and writes Secrets with Build, Create, Apply, Delete and DeleteIfOwned.
serviceaccounts
Package serviceaccounts builds and writes ServiceAccounts with Build, Create, Delete and DeleteIfOwned.
Package serviceaccounts builds and writes ServiceAccounts with Build, Create, Delete and DeleteIfOwned.
services
Package services builds and writes Services with Build, Create, Apply, Delete and DeleteIfOwned.
Package services builds and writes Services with Build, Create, Apply, Delete and DeleteIfOwned.
statefulsets
Package statefulsets builds and writes StatefulSets with Build, Create, Apply, Delete and DeleteIfOwned.
Package statefulsets builds and writes StatefulSets with Build, Create, Apply, Delete and DeleteIfOwned.
types
Package types holds the field types a resource spec is made of: env and envFrom, resource requirements, probes, pod and container security, volumes, rolling update, HPA behavior, network policy rules, limit range items, labels and custom resource metadata.
Package types holds the field types a resource spec is made of: env and envFrom, resource requirements, probes, pod and container security, volumes, rolling update, HPA behavior, network policy rules, limit range items, labels and custom resource metadata.
Command examples shows kubewright building the resources an App owns: presets expanded into fields, then each verb's result, then deletion that leaves another owner's objects alone.
Command examples shows kubewright building the resources an App owns: presets expanded into fields, then each verb's result, then deletion that leaves another owner's objects alone.
internal
testutil
Package testutil holds what the kind packages' tests share: an owner to build for, and a check of the verbs' results against a fake clientset.
Package testutil holds what the kind packages' tests share: an owner to build for, and a check of the verbs' results against a fake clientset.
Package presets names the field settings teams repeat: resource requests and limits, probe timings, container and pod security, rolling update, HPA and PDB behavior, network policy rules and namespace quotas.
Package presets names the field settings teams repeat: resource requests and limits, probe timings, container and pod security, rolling update, HPA and PDB behavior, network policy rules and namespace quotas.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL