Skip to main content

Waiting

Controllers act asynchronously, so tests wait for results. petri's waits watch the API server and return as soon as the result is there. You do not need sleeps or polling loops.

Waiting for one object​

petri.WaitFor(t, client, name, condition) follows one object until the condition holds. It fails the test after 30 seconds. The client is any typed client-go resource client, such as cs.AppsV1().Deployments(ns).

deleted_test.go
package waiting_test

import (
"testing"

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

"github.com/yyewolf/petri"
)

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

cp := petri.Start(t)
configMaps := cp.Client().CoreV1().ConfigMaps(cp.Namespace(t))

owner, err := configMaps.Create(t.Context(), &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "owner"},
}, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
_, err = configMaps.Create(t.Context(), &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "dependent",
OwnerReferences: []metav1.OwnerReference{{
APIVersion: "v1", Kind: "ConfigMap", Name: owner.Name, UID: owner.UID,
}},
},
}, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}

err = configMaps.Delete(t.Context(), owner.Name, metav1.DeleteOptions{})
if err != nil {
t.Fatal(err)
}

// The garbage collector runs on every plane, so the dependent goes too.
petri.WaitFor(t, configMaps, "dependent", petri.Deleted[*corev1.ConfigMap]())
}

The wait lists the object, then watches from its resourceVersion, so it does not miss a change made just before the wait started. If the watch expires, the wait opens a new one. On timeout, the failure message shows the last state the wait saw.

A condition is a func(O) (bool, error). It receives nil while the object does not exist. If it returns an error, the wait stops with that error. petri provides these conditions:

ConditionHolds when
petri.Exists[O]()the object exists
petri.Deleted[O]()the object does not exist
petri.StatusCondition[O](type, status)status.conditions[type] has that status, for any object whose conditions have string type and status fields

petri.WithTimeout(d) changes the 30-second default. An earlier context deadline still wins.

Custom resources usually have no typed client. Wrap a dynamic ResourceInterface with petri.DynamicResource instead; the CRDs guide shows how.

Waiting for anything else​

petri.Retry(t, func(ctx) (bool, error)) calls your function until it returns true. Use it when the result is not the state of one named object:

retry_test.go
package waiting_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 TestDeploymentGetsAReplicaSet(t *testing.T) {
t.Parallel()

cp := petri.Start(t)
ns := cp.Namespace(t)
cs := cp.Client()
cp.EnableControllers(t, petri.DeploymentController)

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

// The ReplicaSet's name is generated, so there is no one object to watch:
// Retry polls a condition of your own until it holds.
petri.Retry(t, func(ctx context.Context) (bool, error) {
list, listErr := cs.AppsV1().ReplicaSets(ns).List(ctx, metav1.ListOptions{LabelSelector: "app=api"})
if listErr != nil {
return false, listErr
}

return len(list.Items) == 1, nil
})
}

Without a testing.T​

petri.WaitUntil(ctx, client, name, condition) and petri.RetryUntil(ctx, condition) return an error instead of failing a test. WaitUntil also returns the last object it saw. See Ginkgo and other frameworks.