Skip to content

Commit 1076877

Browse files
author
Naadir Jeewa
committed
e2e framework: Add conformance testing to test framework
1 parent 3462a5c commit 1076877

File tree

9 files changed

+326
-12
lines changed

9 files changed

+326
-12
lines changed

test/e2e/common.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,12 @@ import (
2121
"fmt"
2222
"path/filepath"
2323

24-
. "github.com/onsi/ginkgo"
25-
2624
"github.com/blang/semver"
2725
"github.com/onsi/gomega/types"
2826
corev1 "k8s.io/api/core/v1"
2927
clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
3028
"sigs.k8s.io/cluster-api/test/framework"
29+
"sigs.k8s.io/cluster-api/test/framework/ginkgoextensions"
3130
"sigs.k8s.io/cluster-api/util"
3231
)
3332

@@ -42,8 +41,9 @@ const (
4241
CoreDNSVersionUpgradeTo = "COREDNS_VERSION_UPGRADE_TO"
4342
)
4443

44+
// Byf is deprecated. Use "sigs.k8s.io/cluster-api/test/framework/ginkgoextensions" as dot import instead.
4545
func Byf(format string, a ...interface{}) {
46-
By(fmt.Sprintf(format, a...))
46+
ginkgoextensions.Byf(format, a...)
4747
}
4848

4949
func setupSpecNamespace(ctx context.Context, specName string, clusterProxy framework.ClusterProxy, artifactFolder string) (*corev1.Namespace, context.CancelFunc) {

test/e2e/self_hosted.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import (
2828
corev1 "k8s.io/api/core/v1"
2929
"k8s.io/utils/pointer"
3030
clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
31-
"sigs.k8s.io/cluster-api/test/e2e/internal/log"
3231
"sigs.k8s.io/cluster-api/test/framework"
3332
"sigs.k8s.io/cluster-api/test/framework/bootstrap"
3433
"sigs.k8s.io/cluster-api/test/framework/clusterctl"
@@ -137,7 +136,7 @@ func SelfHostedSpec(ctx context.Context, inputGetter func() SelfHostedSpecInput)
137136
Namespace: namespace.Name,
138137
})
139138

140-
log.Logf("Waiting for the cluster infrastructure to be provisioned")
139+
By("Waiting for the cluster infrastructure to be provisioned")
141140
selfHostedCluster = framework.DiscoveryAndWaitForCluster(ctx, framework.DiscoveryAndWaitForClusterInput{
142141
Getter: selfHostedClusterProxy.GetClient(),
143142
Namespace: selfHostedNamespace.Name,
@@ -174,7 +173,7 @@ func SelfHostedSpec(ctx context.Context, inputGetter func() SelfHostedSpecInput)
174173
Namespace: selfHostedNamespace.Name,
175174
})
176175

177-
log.Logf("Waiting for the cluster infrastructure to be provisioned")
176+
By("Waiting for the cluster infrastructure to be provisioned")
178177
cluster = framework.DiscoveryAndWaitForCluster(ctx, framework.DiscoveryAndWaitForClusterInput{
179178
Getter: input.BootstrapClusterProxy.GetClient(),
180179
Namespace: namespace.Name,

test/framework/alltypes_helpers.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ import (
2222
"fmt"
2323
"io/ioutil"
2424
"os"
25+
"os/exec"
2526
"path"
2627
"path/filepath"
2728

29+
"github.com/onsi/ginkgo"
2830
. "github.com/onsi/ginkgo"
2931
. "github.com/onsi/gomega"
32+
. "sigs.k8s.io/cluster-api/test/framework/ginkgoextensions"
3033

3134
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
3235
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -182,3 +185,16 @@ func PrettyPrint(v interface{}) string {
182185
}
183186
return string(b)
184187
}
188+
189+
// CompleteCommand prints a command before running it. Acts as a helper function.
190+
// privateArgs when true will not print arguments.
191+
func CompleteCommand(cmd *exec.Cmd, desc string, privateArgs bool) *exec.Cmd {
192+
cmd.Stderr = ginkgo.GinkgoWriter
193+
cmd.Stdout = ginkgo.GinkgoWriter
194+
if privateArgs {
195+
Byf("%s: dir=%s, command=%s", desc, cmd.Dir, cmd)
196+
} else {
197+
Byf("%s: dir=%s, command=%s", desc, cmd.Dir, cmd.String())
198+
}
199+
return cmd
200+
}

test/framework/clusterctl/e2e_config.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,9 +385,18 @@ func (c *E2EConfig) GetIntervals(spec, key string) []interface{} {
385385

386386
// GetVariable returns a variable from the e2e config file.
387387
func (c *E2EConfig) GetVariable(varName string) string {
388-
version, ok := c.Variables[varName]
388+
variable, ok := c.Variables[varName]
389389
Expect(ok).NotTo(BeFalse())
390-
return version
390+
return variable
391+
}
392+
393+
// GetStringVariableWithDefault returns a variable from the e2e config file, but can return a default string.
394+
func (c *E2EConfig) GetStringVariableWithDefault(varName string, defaultValue string) string {
395+
variable, ok := c.Variables[varName]
396+
if !ok {
397+
return defaultValue
398+
}
399+
return variable
391400
}
392401

393402
// GetVariable returns an Int64Ptr variable from the e2e config file.

test/framework/config.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import (
2222
"io"
2323
"io/ioutil"
2424
"net/http"
25+
"os"
26+
"path"
2527
"regexp"
2628

2729
"github.com/pkg/errors"
@@ -363,3 +365,15 @@ func (g componentSourceGenerator) GetName() string {
363365
func (g componentSourceGenerator) Manifests(ctx context.Context) ([]byte, error) {
364366
return YAMLForComponentSource(ctx, g.ComponentSource)
365367
}
368+
369+
// DumpToFile will dump the running e2e config to a file
370+
func (c *Config) DumpToFile(filename string) error {
371+
yaml, err := yaml.Marshal(c)
372+
if err != nil {
373+
return err
374+
}
375+
if err := os.MkdirAll(path.Dir(filename), 0o700); err != nil {
376+
return err
377+
}
378+
return ioutil.WriteFile(filename, yaml, 0o600)
379+
}

test/e2e/internal/log/log.go renamed to test/framework/ginkgoextensions/output.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,25 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
package log
17+
package ginkgoextensions
1818

1919
import (
2020
"fmt"
2121

22-
. "github.com/onsi/ginkgo"
22+
"github.com/go-logr/logr"
23+
"github.com/onsi/ginkgo"
24+
"k8s.io/klog"
25+
"k8s.io/klog/klogr"
2326
)
2427

25-
func Logf(format string, a ...interface{}) {
26-
fmt.Fprintf(GinkgoWriter, "INFO: "+format+"\n", a...)
28+
var Log logr.Logger
29+
30+
func init() {
31+
klog.InitFlags(nil)
32+
Log = klogr.New()
33+
klog.SetOutput(ginkgo.GinkgoWriter)
34+
}
35+
36+
func Byf(format string, a ...interface{}) {
37+
ginkgo.By(fmt.Sprintf(format, a...))
2738
}

test/framework/kubetest/setup.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
Copyright 2020 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 kubetest
18+
19+
import (
20+
"io/ioutil"
21+
"net/http"
22+
"strings"
23+
)
24+
25+
const (
26+
ciVersionURL = "https://dl.k8s.io/ci/latest.txt"
27+
)
28+
29+
// FetchKubernetesCIVersion fetches the latest main branch Kubernetes version
30+
func FetchKubernetesCIVersion() (string, error) {
31+
resp, err := http.Get(ciVersionURL)
32+
if err != nil {
33+
return "", err
34+
}
35+
defer resp.Body.Close()
36+
b, err := ioutil.ReadAll(resp.Body)
37+
if err != nil {
38+
return "", err
39+
}
40+
return strings.TrimSpace(string(b)), nil
41+
}

test/framework/kubetest/test.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
Copyright 2020 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 kubetest
18+
19+
import (
20+
"io/ioutil"
21+
"os"
22+
"os/exec"
23+
"os/user"
24+
"path"
25+
"strconv"
26+
"strings"
27+
28+
"github.com/pkg/errors"
29+
corev1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
"sigs.k8s.io/cluster-api/test/framework"
31+
)
32+
33+
const (
34+
standardImage = "gcr.io/k8s-artifacts-prod/conformance"
35+
ciArtifactImage = "gcr.io/kubernetes-ci-images/conformance"
36+
)
37+
38+
const (
39+
DefaultGinkgoNodes = 1
40+
DefaultGinkoSlowSpecThreshold = 120
41+
)
42+
43+
type RunInput struct {
44+
// ClusterProxy is a clusterctl test framework proxy for the workload cluster
45+
// for which to run kubetest against
46+
ClusterProxy framework.ClusterProxy
47+
// NumberOfNodes is the number of cluster nodes that exist for kubetest
48+
// to be aware of
49+
NumberOfNodes int
50+
// UseCIArtifacts will fetch the latest build from the main branch of kubernetes/kubernetes
51+
UseCIArtifacts bool `json:"useCIArtifacts,omitempty"`
52+
// ArtifactsDirectory is where conformance suite output will go
53+
ArtifactsDirectory string `json:"artifactsDirectory,omitempty"`
54+
// Path to the kubetest e2e config file
55+
ConfigFilePath string `json:"configFilePath,omitempty"`
56+
// GinkgoNodes is the number of Ginkgo nodes to use
57+
GinkgoNodes int `json:"ginkgoNodes,omitempty"`
58+
// GinkgoSlowSpecThreshold is time in s before spec is marked as slow
59+
GinkgoSlowSpecThreshold int `json:"ginkgoSlowSpecThreshold,omitempty"`
60+
// KubernetesVersion is the version of Kubernetes to test
61+
KubernetesVersion string
62+
// ConformanceImage is an optional field to specify an exact conformance image
63+
ConformanceImage string
64+
}
65+
66+
// Run executes kube-test given an artifact directory, and sets settings
67+
// required for kubetest to work with Cluster API. JUnit files are
68+
// also gathered for inclusion in Prow.
69+
func Run(input RunInput) error {
70+
if input.GinkgoNodes == 0 {
71+
input.GinkgoNodes = DefaultGinkgoNodes
72+
}
73+
if input.GinkgoSlowSpecThreshold == 0 {
74+
input.GinkgoSlowSpecThreshold = 120
75+
}
76+
if input.NumberOfNodes == 0 {
77+
numNodes, err := countClusterNodes(input.ClusterProxy)
78+
if err != nil {
79+
return err
80+
}
81+
input.NumberOfNodes = numNodes
82+
}
83+
if input.ArtifactsDirectory == "" {
84+
if dir, ok := os.LookupEnv("ARTIFACTS"); ok {
85+
input.ArtifactsDirectory = dir
86+
}
87+
input.ArtifactsDirectory = "_artifacts"
88+
}
89+
reportDir := path.Join(input.ArtifactsDirectory, "kubetest")
90+
outputDir := path.Join(reportDir, "e2e-output")
91+
if err := os.MkdirAll(outputDir, 0o750); err != nil {
92+
return err
93+
}
94+
ginkgoVars := map[string]string{
95+
"nodes": strconv.Itoa(input.GinkgoNodes),
96+
"slowSpecThreshold": strconv.Itoa(input.GinkgoSlowSpecThreshold),
97+
}
98+
e2eVars := map[string]string{
99+
"kubeconfig": "/tmp/kubeconfig",
100+
"provider": "skeleton",
101+
"report-dir": "/output",
102+
"e2e-output-dir": "/output/e2e-output",
103+
"dump-logs-on-failure": "false",
104+
"report-prefix": "kubetest.",
105+
"num-nodes": strconv.FormatInt(int64(input.NumberOfNodes), 10),
106+
"viper-config": "/tmp/viper-config.yaml",
107+
}
108+
ginkgoArgs := buildArgs(ginkgoVars, "-")
109+
e2eArgs := buildArgs(e2eVars, "--")
110+
if input.ConformanceImage == "" {
111+
input.ConformanceImage = versionToConformanceImage(input.KubernetesVersion, input.UseCIArtifacts)
112+
}
113+
kubeConfigVolumeMount := volumeArg(input.ClusterProxy.GetKubeconfigPath(), "/tmp/kubeconfig")
114+
outputVolumeMount := volumeArg(reportDir, "/output")
115+
viperVolumeMount := volumeArg(input.ConfigFilePath, "/tmp/viper-config.yaml")
116+
user, err := user.Current()
117+
if err != nil {
118+
return errors.Wrap(err, "unable to determine current user")
119+
}
120+
userArg := user.Uid + ":" + user.Gid
121+
e2eCmd := exec.Command("docker", "run", "--user", userArg, kubeConfigVolumeMount, outputVolumeMount, viperVolumeMount, "-t", input.ConformanceImage)
122+
e2eCmd.Args = append(e2eCmd.Args, "/usr/local/bin/ginkgo")
123+
e2eCmd.Args = append(e2eCmd.Args, ginkgoArgs...)
124+
e2eCmd.Args = append(e2eCmd.Args, "/usr/local/bin/e2e.test")
125+
e2eCmd.Args = append(e2eCmd.Args, "--")
126+
e2eCmd.Args = append(e2eCmd.Args, e2eArgs...)
127+
e2eCmd = framework.CompleteCommand(e2eCmd, "Running e2e test", false)
128+
if err := e2eCmd.Run(); err != nil {
129+
return errors.Wrap(err, "Unable to run conformance tests")
130+
}
131+
if err := framework.GatherJUnitReports(reportDir, input.ArtifactsDirectory); err != nil {
132+
return err
133+
}
134+
return nil
135+
}
136+
137+
func countClusterNodes(proxy framework.ClusterProxy) (int, error) {
138+
nodeList, err := proxy.GetClientSet().CoreV1().Nodes().List(corev1.ListOptions{})
139+
if err != nil {
140+
return 0, errors.Wrap(err, "Unable to count nodes")
141+
}
142+
return len(nodeList.Items), nil
143+
}
144+
145+
func isSELinuxEnforcing() bool {
146+
dat, err := ioutil.ReadFile("/sys/fs/selinux/enforce")
147+
if err != nil {
148+
return false
149+
}
150+
return string(dat) == "1"
151+
}
152+
153+
func volumeArg(src, dest string) string {
154+
volumeArg := "-v" + src + ":" + dest
155+
if isSELinuxEnforcing() {
156+
return volumeArg + ":z"
157+
}
158+
return volumeArg
159+
}
160+
161+
func versionToConformanceImage(kubernetesVersion string, usingCIArtifacts bool) string {
162+
k8sVersion := strings.ReplaceAll(kubernetesVersion, "+", "_")
163+
if usingCIArtifacts {
164+
return ciArtifactImage + ":" + k8sVersion
165+
}
166+
return standardImage + ":" + k8sVersion
167+
}
168+
169+
// buildArgs converts a string map to the format --key=value
170+
func buildArgs(kv map[string]string, flagMarker string) []string {
171+
args := make([]string, len(kv))
172+
i := 0
173+
for k, v := range kv {
174+
args[i] = flagMarker + k + "=" + v
175+
i++
176+
}
177+
return args
178+
}

0 commit comments

Comments
 (0)