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).
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:
| Condition | Holds 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:
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.