Skip to main content

Built-in controllers

petri runs the upstream controllers from kube-controller-manager inside your test process. It does not run kube-controller-manager itself. Instead, a small manager starts each controller, with shared informers, when a test asks for it.

On by default​

Every plane runs two controllers:

  • the namespace controller, which empties and removes a deleted namespace;
  • the garbage collector, which deletes dependents when their owner is deleted, with background, foreground and orphan propagation, for built-in kinds and custom resources.

Start and New return once both have synced, so they see the first object a test creates. This is the main difference from envtest. A dedicated plane started with petri.WithoutDefaultControllers() runs neither.

Enabling the others​

cp.EnableControllers(t, controllers...) starts controllers and returns once they have synced, so they see an object created on the next line.

nodeless_test.go
package controllers_test

import (
"context"
"testing"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/yyewolf/petri"
)

func TestReplicaSetCreatesPendingPods(t *testing.T) {
t.Parallel()

cp := petri.Start(t)
ns := cp.Namespace(t)
cs := cp.Client()
// Enabling a controller never enables another: Deployment alone would
// create the ReplicaSet and stop there.
cp.EnableControllers(t, petri.DeploymentController, petri.ReplicaSetController)

labels := map[string]string{"app": "web"}
_, err := cs.AppsV1().Deployments(ns).Create(t.Context(), &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "web"},
Spec: appsv1.DeploymentSpec{
Replicas: new(int32(3)),
Selector: &metav1.LabelSelector{MatchLabels: labels},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: labels},
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "web", Image: "nginx"}}},
},
},
}, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}

// The shared plane has no Nodes, so the three Pods exist but stay Pending.
petri.Retry(t, func(ctx context.Context) (bool, error) {
pods, listErr := cs.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{LabelSelector: "app=web"})
if listErr != nil {
return false, listErr
}
for _, pod := range pods.Items {
if pod.Status.Phase != corev1.PodPending {
return false, nil
}
}

return len(pods.Items) == 3, nil
})
}
  • Parallel tests can enable the same controller safely. On the shared plane, a controller stays on for every later test once one test enables it.
  • Enabling a controller never enables another. The Deployment controller on its own creates ReplicaSets but no Pods.
  • petri.WorkloadControllers() returns all the workload controllers.
ControllerWhat it does
DeploymentControllerRolls Deployments out through ReplicaSets.
ReplicaSetControllerKeeps spec.replicas Pods per ReplicaSet.
StatefulSetControllerCreates StatefulSet Pods in order.
DaemonSetControllerPlaces one Pod per eligible Node.
JobControllerRuns Jobs' Pods and marks Jobs complete or failed.
CronJobControllerCreates Jobs on a schedule.
TTLAfterFinishedControllerDeletes finished Jobs after ttlSecondsAfterFinished.
EndpointSliceControllerKeeps an EndpointSlice per Service with a selector.
ServiceAccountControllerKeeps a default ServiceAccount in every namespace.
ServiceAccountTokenControllerFills legacy service-account token Secrets with a token the API server accepts.
ResourceQuotaControllerKeeps quotas' status.used current. Quota admission is on regardless, so hard limits are enforced even with the controller off.
DisruptionControllerComputes PodDisruptionBudget statuses.
SchedulerControllerkube-scheduler; see Nodes and scheduling.

Without Nodes​

The shared plane has no Nodes, and a dedicated plane has none until a test adds them. Without Nodes, Pods stay Pending, and each workload controller stops at the point where it needs a running Pod:

ControllerOn a plane without Nodes
Deployment, ReplicaSetCreate Pods, which stay Pending; Deployments never become Available.
StatefulSetWith the default OrderedReady policy, creates Pod 0 and waits for it forever.
DaemonSetCreates no Pods: there is no Node to run them on.
JobCreates Pods up to parallelism; the Job never completes.
EndpointSliceCreates EndpointSlices with no endpoints: no Pod is ever Ready.
DisruptionEvery budget settles on zero disruptionsAllowed.

Many tests need no more than this. If your operator creates Deployments, you can check what it created without running anything. To test further, add Nodes on a dedicated plane. Deployments then become Available and StatefulSets roll out, and Jobs complete when the test completes their Pods.

CronJobs​

The CronJob controller follows the wall clock, and upstream has no way to inject a fake one. To test what a CronJob runs, create a Job from its spec.jobTemplate in the test instead of waiting for the schedule.