Skip to content

Commit 67ca656

Browse files
committed
add condition helper for checking deployment status and methods for waiting on object lists
Signed-off-by: Chris Randles <[email protected]>
1 parent 55d8b7e commit 67ca656

File tree

7 files changed

+639
-13
lines changed

7 files changed

+639
-13
lines changed

examples/wait_for_resources/README.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Waiting for Resource Changes
2+
3+
The test harness supports several methods for querying Kubernetes object types and waiting for conditions to be met. This example shows how to create various wait conditions to drive your tests.
4+
5+
## Waiting for a single object
6+
7+
The wait package has built-in with utilities for waiting on Pods, Jobs, and Deployments:
8+
9+
```go
10+
func TestPodRunning(t *testing.T) {
11+
var err error
12+
pod := v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "my-pod"}}
13+
err = wait.For(conditions.New(client.Resources()).PodRunning(pod), WithImmediate())
14+
if err != nil {
15+
t.Error(err)
16+
}
17+
}
18+
```
19+
20+
Additionally, it is easy to wait for changes to any resource type with the `ResourceMatch` method:
21+
22+
```go
23+
func TestResourceMatch(t *testing.T) {
24+
...
25+
deployment := appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "deploy-name"}}
26+
err = wait.For(conditions.New(client.Resources()).ResourceMatch(deployment, func(object k8s.Object) bool {
27+
d := object.(*appsv1.Deployment)
28+
return d.Status.AvailableReplicas == 2 && d.Status.ReadyReplicas == 2
29+
}))
30+
if err != nil {
31+
t.Error(err)
32+
}
33+
...
34+
}
35+
```
36+
37+
## Waiting for a lists of objects
38+
39+
It is common to need to check for the existence of a set of objects by name:
40+
41+
```go
42+
func TestResourcesFound(t *testing.T) {
43+
...
44+
pods := &v1.PodList{
45+
Items: []v1.Pod{
46+
{ObjectMeta: metav1.ObjectMeta{Name: "p9", Namespace: namespace}},
47+
{ObjectMeta: metav1.ObjectMeta{Name: "p10", Namespace: namespace}},
48+
{ObjectMeta: metav1.ObjectMeta{Name: "p11", Namespace: namespace}},
49+
},
50+
}
51+
// wait for the set of pods to exist
52+
err = wait.For(conditions.New(client.Resources()).ResourcesFound(pods))
53+
if err != nil {
54+
t.Error(err)
55+
}
56+
...
57+
}
58+
```
59+
60+
Or to check for their absence:
61+
62+
```go
63+
func TestResourcesDeleted(t *testing.T) {
64+
...
65+
pods := &v1.PodList{}
66+
// wait for 1 pod with the label `"app": "d5"`
67+
err = wait.For(conditions.New(client.Resources()).ResourceListN(
68+
pods,
69+
1,
70+
resources.WithLabelSelector(labels.FormatLabels(map[string]string{"app": "d5"}))),
71+
)
72+
if err != nil {
73+
t.Error(err)
74+
}
75+
err = client.Resources().Delete(context.Background(), deployment)
76+
if err != nil {
77+
t.Error(err)
78+
}
79+
// wait for the set of pods to finish deleting
80+
err = wait.For(conditions.New(client.Resources()).ResourcesDeleted(pods))
81+
if err != nil {
82+
t.Error(err)
83+
}
84+
...
85+
}
86+
```
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package wait_for_resources
18+
19+
import (
20+
"os"
21+
"testing"
22+
23+
"sigs.k8s.io/e2e-framework/pkg/env"
24+
"sigs.k8s.io/e2e-framework/pkg/envconf"
25+
"sigs.k8s.io/e2e-framework/pkg/envfuncs"
26+
)
27+
28+
var testenv env.Environment
29+
30+
func TestMain(m *testing.M) {
31+
testenv = env.New()
32+
kindClusterName := envconf.RandomName("wait-for-resources", 16)
33+
namespace := envconf.RandomName("kind-ns", 16)
34+
testenv.Setup(
35+
envfuncs.CreateKindCluster(kindClusterName),
36+
envfuncs.CreateNamespace(namespace),
37+
)
38+
testenv.Finish(
39+
envfuncs.DeleteNamespace(namespace),
40+
envfuncs.DestroyKindCluster(kindClusterName),
41+
)
42+
os.Exit(testenv.Run(m))
43+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package wait_for_resources
18+
19+
import (
20+
"context"
21+
"testing"
22+
"time"
23+
24+
appsv1 "k8s.io/api/apps/v1"
25+
v1 "k8s.io/api/core/v1"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/apimachinery/pkg/labels"
28+
"sigs.k8s.io/e2e-framework/klient/k8s"
29+
"sigs.k8s.io/e2e-framework/klient/k8s/resources"
30+
"sigs.k8s.io/e2e-framework/klient/wait"
31+
"sigs.k8s.io/e2e-framework/klient/wait/conditions"
32+
"sigs.k8s.io/e2e-framework/pkg/envconf"
33+
"sigs.k8s.io/e2e-framework/pkg/features"
34+
)
35+
36+
func TestWaitForResources(t *testing.T) {
37+
depFeature := features.New("appsv1/deployment").WithLabel("env", "dev").
38+
Setup(func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
39+
// create a deployment
40+
deployment := newDeployment(cfg.Namespace(), "test-deployment", 10)
41+
client, err := cfg.NewClient()
42+
if err != nil {
43+
t.Fatal(err)
44+
}
45+
if err := client.Resources().Create(ctx, deployment); err != nil {
46+
t.Fatal(err)
47+
}
48+
return ctx
49+
}).
50+
Assess("deployment >=50% available", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
51+
client, err := cfg.NewClient()
52+
if err != nil {
53+
t.Fatal(err)
54+
}
55+
dep := appsv1.Deployment{
56+
ObjectMeta: metav1.ObjectMeta{Name: "test-deployment", Namespace: cfg.Namespace()},
57+
}
58+
// wait for the deployment to become at least 50%
59+
err = wait.For(conditions.New(client.Resources()).ResourceMatch(&dep, func(object k8s.Object) bool {
60+
d := object.(*appsv1.Deployment)
61+
return float64(d.Status.ReadyReplicas)/float64(*d.Spec.Replicas) >= 0.50
62+
}), wait.WithTimeout(time.Minute*1))
63+
if err != nil {
64+
t.Fatal(err)
65+
}
66+
t.Logf("deployment availability: %.2f%%", float64(dep.Status.ReadyReplicas)/float64(*dep.Spec.Replicas)*100)
67+
return ctx
68+
}).
69+
Assess("deployment available", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
70+
client, err := cfg.NewClient()
71+
if err != nil {
72+
t.Fatal(err)
73+
}
74+
dep := appsv1.Deployment{
75+
ObjectMeta: metav1.ObjectMeta{Name: "test-deployment", Namespace: cfg.Namespace()},
76+
}
77+
// wait for the deployment to finish becoming available
78+
err = wait.For(conditions.New(client.Resources()).DeploymentConditionMatch(&dep, appsv1.DeploymentAvailable, v1.ConditionTrue), wait.WithTimeout(time.Minute*1))
79+
if err != nil {
80+
t.Fatal(err)
81+
}
82+
return ctx
83+
}).
84+
Assess("deployment pod garbage collection", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
85+
client, err := cfg.NewClient()
86+
if err != nil {
87+
t.Fatal(err)
88+
}
89+
// get list of pods
90+
var pods v1.PodList
91+
err = client.Resources(cfg.Namespace()).List(context.TODO(), &pods, resources.WithLabelSelector(labels.FormatLabels(map[string]string{"app": "wait-for-resources"})))
92+
if err != nil {
93+
t.Fatal(err)
94+
}
95+
// delete the deployment
96+
dep := appsv1.Deployment{
97+
ObjectMeta: metav1.ObjectMeta{Name: "test-deployment", Namespace: cfg.Namespace()},
98+
}
99+
err = client.Resources(cfg.Namespace()).Delete(context.TODO(), &dep)
100+
if err != nil {
101+
t.Fatal(err)
102+
}
103+
// wait for the deployment pods to be deleted
104+
err = wait.For(conditions.New(client.Resources()).ResourcesDeleted(&pods), wait.WithTimeout(time.Minute*1))
105+
if err != nil {
106+
t.Fatal(err)
107+
}
108+
return ctx
109+
}).Feature()
110+
111+
testenv.Test(t, depFeature)
112+
}
113+
114+
func newDeployment(namespace string, name string, replicas int32) *appsv1.Deployment {
115+
return &appsv1.Deployment{
116+
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace, Labels: map[string]string{"app": "wait-for-resources"}},
117+
Spec: appsv1.DeploymentSpec{
118+
Replicas: &replicas,
119+
Selector: &metav1.LabelSelector{
120+
MatchLabels: map[string]string{"app": "wait-for-resources"},
121+
},
122+
Template: v1.PodTemplateSpec{
123+
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "wait-for-resources"}},
124+
Spec: v1.PodSpec{Containers: []v1.Container{{Name: "nginx", Image: "nginx"}}},
125+
},
126+
},
127+
}
128+
}

klient/internal/testutil/setup.go

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ limitations under the License.
1717
package testutil
1818

1919
import (
20+
"context"
2021
"time"
2122

22-
log "k8s.io/klog/v2"
23-
23+
v1 "k8s.io/api/core/v1"
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
"k8s.io/apimachinery/pkg/util/wait"
2426
"k8s.io/client-go/kubernetes"
2527
"k8s.io/client-go/rest"
28+
log "k8s.io/klog/v2"
2629
"sigs.k8s.io/e2e-framework/klient/conf"
2730
"sigs.k8s.io/e2e-framework/support/kind"
2831
)
@@ -57,6 +60,9 @@ func SetupTestCluster(path string) *TestCluster {
5760
log.Fatalln("failed to create new Client set for kind cluster", err)
5861
}
5962
tc.Clientset = clientSet
63+
if err := waitForControlPlane(clientSet); err != nil {
64+
log.Fatalln("failed to wait for Kind Cluster control-plane components", err)
65+
}
6066
return tc
6167
}
6268

@@ -73,9 +79,67 @@ func setupKind() (kc *kind.Cluster, err error) {
7379
if _, err = kc.Create(); err != nil {
7480
return
7581
}
76-
77-
waitPeriod := 10 * time.Second
78-
log.Info("Waiting for kind pods to be initialized...")
79-
time.Sleep(waitPeriod)
8082
return
8183
}
84+
85+
func waitForControlPlane(c kubernetes.Interface) error {
86+
selector, err := metav1.LabelSelectorAsSelector(
87+
&metav1.LabelSelector{
88+
MatchExpressions: []metav1.LabelSelectorRequirement{
89+
{Key: "component", Operator: metav1.LabelSelectorOpIn, Values: []string{"etcd", "kube-apiserver", "kube-controller-manager", "kube-scheduler"}},
90+
},
91+
},
92+
)
93+
if err != nil {
94+
return err
95+
}
96+
options := metav1.ListOptions{LabelSelector: selector.String()}
97+
log.Info("Waiting for kind control-plane pods to be initialized...")
98+
err = wait.Poll(5*time.Second, time.Minute*2,
99+
func() (bool, error) {
100+
pods, err := c.CoreV1().Pods("kube-system").List(context.TODO(), options)
101+
if err != nil {
102+
return false, err
103+
}
104+
running := 0
105+
for i := range pods.Items {
106+
if pods.Items[i].Status.Phase == v1.PodRunning {
107+
running++
108+
}
109+
}
110+
// a kind cluster with one control-plane node will have 4 pods running the core apiserver components
111+
return running >= 4, nil
112+
})
113+
if err != nil {
114+
return err
115+
}
116+
117+
selector, err = metav1.LabelSelectorAsSelector(
118+
&metav1.LabelSelector{
119+
MatchExpressions: []metav1.LabelSelectorRequirement{
120+
{Key: "k8s-app", Operator: metav1.LabelSelectorOpIn, Values: []string{"kindnet", "kube-dns", "kube-proxy"}},
121+
},
122+
},
123+
)
124+
if err != nil {
125+
return err
126+
}
127+
options = metav1.ListOptions{LabelSelector: selector.String()}
128+
log.Info("Waiting for kind networking pods to be initialized...")
129+
err = wait.Poll(5*time.Second, time.Minute*2,
130+
func() (bool, error) {
131+
pods, err := c.CoreV1().Pods("kube-system").List(context.TODO(), options)
132+
if err != nil {
133+
return false, err
134+
}
135+
running := 0
136+
for i := range pods.Items {
137+
if pods.Items[i].Status.Phase == v1.PodRunning {
138+
running++
139+
}
140+
}
141+
// a kind cluster with one control-plane node will have 4 k8s-app pods running networking components
142+
return running >= 4, nil
143+
})
144+
return err
145+
}

0 commit comments

Comments
 (0)