Skip to content

Commit 0402e81

Browse files
authored
Merge pull request #24 from vladimirvivien/context-fix-example
Context propagation fix and context examples
2 parents 1a6ef66 + 9e187de commit 0402e81

22 files changed

+787
-324
lines changed

docs/design/test-harness-framework.md

+91-110
Large diffs are not rendered by default.

examples/k8s/README.md

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Testing Kubernetes Resources
2+
3+
This package demonstrates how to use all part of the framework to setup and run an
4+
end-to-end test that retrieves API objects from Kubernetes for assessments. The test
5+
is split into two parts, the test suite definition and the test functions.
6+
7+
## Test suite definition
8+
File `main_tests.go` sets up the test suite for the package.
9+
10+
### Declare the global
11+
The first step is to declare the global environment that will used to run the test.
12+
13+
```go
14+
var (
15+
testenv env.Environment
16+
)
17+
18+
func TestMain(m *testing.M) {
19+
testenv = env.New()
20+
}
21+
```
22+
23+
### Define environment `Setup` functions
24+
The next step is to define some environment functions. Here we have two `Setup` EnvFuncs
25+
to do the following setups:
26+
* The first `Setup` func, uses functions from `kindsupport.go`, to create a kind cluster and gets the kubeconfig file as a result
27+
* The second setup func, creates a `klient.Client` type used to interact with the API server
28+
```go
29+
func TestMain(m *testing.M) {
30+
testenv = env.New()
31+
testenv.Setup(
32+
// env func: creates kind cluster, propagate kubeconfig file name
33+
func(ctx context.Context, cfg *envconf.Config) (context.Context, error) {
34+
cluster := envconf.RandomName("my-cluster", 16)
35+
kubecfg, err := createKindCluster(cluster)
36+
if err != nil {
37+
return ctx, err
38+
}
39+
// stall a bit to allow most pods to come up
40+
time.Sleep(time.Second*10)
41+
42+
// propagate cluster name and kubeconfig file name
43+
return context.WithValue(context.WithValue(ctx, 1, kubecfg), 2, cluster), nil
44+
},
45+
// env func: creates a klient.Client for the envconfig.Config
46+
func(ctx context.Context, cfg *envconf.Config) (context.Context, error) {
47+
kubecfg := ctx.Value(1).(string)
48+
// create a klient.Client and set it for the env config
49+
client, err := klient.NewWithKubeConfigFile(kubecfg)
50+
if err != nil {
51+
return ctx, fmt.Errorf("create klient.Client: %w", err)
52+
}
53+
cfg.WithClient(client) // set client in envconfig
54+
return ctx, nil
55+
},
56+
)
57+
...
58+
}
59+
```
60+
61+
### Define environment `Finish` for cleanup
62+
The last step in creating the suite is to define cleanup code for the environment.
63+
64+
```go
65+
func TestMain(m *testing.M) {
66+
testenv = env.New()
67+
...
68+
testenv.Finish(
69+
// Teardown func: delete kind cluster and delete kubecfg file
70+
func(ctx context.Context, cfg *envconf.Config) (context.Context, error) {
71+
kubecfg := ctx.Value(1).(string)
72+
cluster := ctx.Value(2).(string)
73+
if err := deleteKindCluster(cluster, kubecfg); err != nil {
74+
return ctx, err
75+
}
76+
return ctx, nil
77+
},
78+
)
79+
}
80+
```
81+
### Start the suite
82+
The last step in defining the test suite is to launch it:
83+
```go
84+
func TestMain(m *testing.M) {
85+
testenv = env.New()
86+
...
87+
os.Exit(testenv.Run(m))
88+
}
89+
```
90+
91+
* Uses support functions, found in `kindsupport.go` to stand up a kind cluster
92+
93+
The test suite uses context propagation to propagate the `kubeconfig` file name and the name of the cluster, that created,
94+
so that they can be cleaned in the `Finish` stage.
95+
96+
## Test functions
97+
The framework uses a regular Go test function to define the end-to-end test. Once a suite is launched, after its setup
98+
is successful, Go test will run the Go test functions in the package.
99+
100+
### Testing Kubernetes
101+
The test functions in this example, found in `k8s_test.go`, use the E2E test harness framework to define a test feature. The framework
102+
allows test authors to inject custom function definitions as hooks that get executed during the test. The following
103+
example defines one test feature with a single assessment. The assessment retrieves pods in the kube-system
104+
namespace and inspect the quantity returned.
105+
106+
```go
107+
func TestListPods(t *testing.T) {
108+
f := features.New("example with klient package").
109+
Assess("get pods from kube-system", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
110+
var pods corev1.PodList
111+
err := cfg.Client().Resources("kube-system").List(context.TODO(), &pods)
112+
if err != nil {
113+
t.Fatal(err)
114+
}
115+
t.Logf("found %d pods", len(pods.Items))
116+
if len(pods.Items) == 0 {
117+
t.Fatal("no pods in namespace kube-system")
118+
}
119+
return ctx
120+
})
121+
122+
testenv.Test(t, f.Feature())
123+
}
124+
```
125+
126+
## Run the test
127+
Use the Go test tool to run the test.
128+
129+
```bash
130+
$ go test . -v
131+
```

examples/k8s/k8s_test.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 k8s
18+
19+
import (
20+
"context"
21+
"testing"
22+
23+
corev1 "k8s.io/api/core/v1"
24+
"sigs.k8s.io/e2e-framework/pkg/envconf"
25+
"sigs.k8s.io/e2e-framework/pkg/features"
26+
)
27+
28+
func TestListPods(t *testing.T) {
29+
f := features.New("example with klient package").
30+
Assess("get pods from kube-system", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
31+
var pods corev1.PodList
32+
err := cfg.Client().Resources("kube-system").List(context.TODO(), &pods)
33+
if err != nil {
34+
t.Fatal(err)
35+
}
36+
t.Logf("found %d pods", len(pods.Items))
37+
if len(pods.Items) == 0 {
38+
t.Fatal("no pods in namespace kube-system")
39+
}
40+
return ctx
41+
})
42+
43+
testenv.Test(t, f.Feature())
44+
}

examples/k8s/kindsupport.go

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 k8s
18+
19+
import (
20+
"fmt"
21+
"io"
22+
"io/ioutil"
23+
"os"
24+
25+
"github.com/vladimirvivien/gexe"
26+
)
27+
28+
var (
29+
e = gexe.New()
30+
kindVersion = "v0.11.0"
31+
)
32+
33+
func createKindCluster(clusterName string) (string, error) {
34+
if e.Prog().Avail("kind") == "" {
35+
fmt.Println(`kind may not be installed, attempting to install...`)
36+
p := e.SetEnv("GO111MODULE", "on").RunProc(fmt.Sprintf("go get sigs.k8s.io/kind@%s", kindVersion))
37+
if p.Err() != nil {
38+
return "", fmt.Errorf("install kind failed: %s: %w", p.Result(), p.Err())
39+
}
40+
e.SetEnv("PATH", e.Run("echo $PATH:$GOPATH/bin"))
41+
}
42+
43+
// create cluster
44+
p := e.RunProc(fmt.Sprintf(`kind create cluster --name %s`, clusterName))
45+
if p.Err() != nil {
46+
return "", fmt.Errorf("kind create cluster: %s: %w", p.Result(), p.Err())
47+
}
48+
49+
// grab kubeconfig file for cluster
50+
kubecfg := fmt.Sprintf("%s-kubecfg", clusterName)
51+
p = e.StartProc(fmt.Sprintf(`kind get kubeconfig --name %s`, clusterName))
52+
if p.Err() != nil {
53+
return "", fmt.Errorf("kind get kubeconfig: %s: %w", p.Result(), p.Err())
54+
}
55+
56+
file, err := ioutil.TempFile("", fmt.Sprintf("kind-cluser-%s", kubecfg))
57+
if err != nil {
58+
return "", fmt.Errorf("kind kubeconfig file: %w", err)
59+
}
60+
defer file.Close()
61+
if n, err := io.Copy(file, p.Out()); n == 0 || err != nil {
62+
return "", fmt.Errorf("kind kubecfg file: bytes copied: %d: %w]", n, err)
63+
}
64+
return file.Name(), nil
65+
}
66+
67+
func deleteKindCluster(clusterName, kubeconfig string) error {
68+
p := e.RunProc(fmt.Sprintf(`kind delete cluster --name %s`, clusterName))
69+
if p.Err() != nil {
70+
return fmt.Errorf("kind delete cluster: %s: %w", p.Result(), p.Err())
71+
}
72+
if err := os.RemoveAll(kubeconfig); err != nil {
73+
return fmt.Errorf("kind: remove kubefconfig failed: %w", err)
74+
}
75+
return nil
76+
}

examples/k8s/main_test.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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 k8s
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"os"
23+
"testing"
24+
"time"
25+
26+
"sigs.k8s.io/e2e-framework/klient"
27+
"sigs.k8s.io/e2e-framework/pkg/env"
28+
"sigs.k8s.io/e2e-framework/pkg/envconf"
29+
)
30+
31+
var (
32+
testenv env.Environment
33+
)
34+
35+
func TestMain(m *testing.M) {
36+
testenv = env.New()
37+
testenv.Setup(
38+
// env func: creates kind cluster, propagate kubeconfig file name
39+
func(ctx context.Context, cfg *envconf.Config) (context.Context, error) {
40+
cluster := envconf.RandomName("my-cluster", 16)
41+
kubecfg, err := createKindCluster(cluster)
42+
if err != nil {
43+
return ctx, err
44+
}
45+
// stall a bit to allow most pods to come up
46+
time.Sleep(time.Second * 10)
47+
48+
// propagate cluster name and kubeconfig file name
49+
return context.WithValue(context.WithValue(ctx, 1, kubecfg), 2, cluster), nil
50+
},
51+
// env func: creates a klient.Client for the envconfig.Config
52+
func(ctx context.Context, cfg *envconf.Config) (context.Context, error) {
53+
kubecfg := ctx.Value(1).(string)
54+
// create a klient.Client and set it for the env config
55+
client, err := klient.NewWithKubeConfigFile(kubecfg)
56+
if err != nil {
57+
return ctx, fmt.Errorf("create klient.Client: %w", err)
58+
}
59+
cfg.WithClient(client) // set client in envconfig
60+
return ctx, nil
61+
},
62+
).Finish(
63+
// Teardown func: delete kind cluster and delete kubecfg file
64+
func(ctx context.Context, cfg *envconf.Config) (context.Context, error) {
65+
kubecfg := ctx.Value(1).(string)
66+
cluster := ctx.Value(2).(string)
67+
if err := deleteKindCluster(cluster, kubecfg); err != nil {
68+
return ctx, err
69+
}
70+
return ctx, nil
71+
},
72+
)
73+
74+
os.Exit(testenv.Run(m))
75+
}

examples/simple/README.md

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Simple examples (without test suites)
2+
3+
As the title implies, this package shows how the test framework can be used
4+
directly in test functions without setting up a test suite in a `TestMain` function.

examples/simple/hello_test.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ func TestHello(t *testing.T) {
3737
e := env.NewWithConfig(envconf.New())
3838
feat := features.New("Hello Feature").
3939
WithLabel("type", "simple").
40-
Assess("test message", func(ctx context.Context, t *testing.T) context.Context {
40+
Assess("test message", func(ctx context.Context, t *testing.T, _ *envconf.Config) context.Context {
4141
result := Hello("foo")
4242
if result != "Hello foo" {
4343
t.Error("unexpected message")
4444
}
4545
return ctx
46-
}).Feature()
46+
})
4747

48-
e.Test(context.TODO(), t, feat)
48+
e.Test(t, feat.Feature())
4949
}
5050

5151
// The following shows an example of a simple
@@ -56,17 +56,17 @@ func TestHello_WithSetup(t *testing.T) {
5656
var name string
5757
feat := features.New("Hello Feature").
5858
WithLabel("type", "simple").
59-
Setup(func(ctx context.Context, t *testing.T) context.Context {
59+
Setup(func(ctx context.Context, t *testing.T, _ *envconf.Config) context.Context {
6060
name = "foobar"
6161
return ctx
6262
}).
63-
Assess("test message", func(ctx context.Context, t *testing.T) context.Context {
63+
Assess("test message", func(ctx context.Context, t *testing.T, _ *envconf.Config) context.Context {
6464
result := Hello(name)
6565
if result != "Hello foobar" {
6666
t.Error("unexpected message")
6767
}
6868
return ctx
6969
}).Feature()
7070

71-
e.Test(context.TODO(), t, feat)
71+
e.Test(t, feat)
7272
}

0 commit comments

Comments
 (0)