Skip to content

Commit 7fa2522

Browse files
committed
Add first crictl e2e test framework and suite
This adds a new Makefile target `test-e2e`, which uses the existing ginkgo framework to run end-to-end tests for `crictl`. The test framework and suite setups CRI-O in a tmp sandbox, whereas every test can specify if a runtime is needed or not. This way it is possible to create a new isolated environment where `crictl` can operate on. Please be aware that external dependencies like `runc` and the CNI plugins have to be existend on the system to acually work. The tests have to be executed as root for now. An example test suite for the `help` as well as the `info` command has been added as well. Signed-off-by: Sascha Grunert <[email protected]>
1 parent 418de71 commit 7fa2522

File tree

14 files changed

+1348
-1
lines changed

14 files changed

+1348
-1
lines changed

.travis.yml

+22
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ before_install:
2525
fi
2626
)
2727
28+
2829
install:
2930
- make install.tools
3031

@@ -64,6 +65,27 @@ jobs:
6465
- powershell -c "Set-ExecutionPolicy Bypass -Scope CURRENTUSER -Force"
6566
- travis_wait powershell hack/install-kubelet.ps1
6667
# Skip hack/run-critest.sh temporarily.
68+
- stage: Test
69+
name: crictl e2e
70+
os: linux
71+
script:
72+
- |
73+
sudo apt-get update &&\
74+
sudo apt-get install -y libseccomp-dev
75+
- |
76+
VERSION=v0.8.2 &&\
77+
sudo mkdir -p /opt/cni/bin &&\
78+
sudo wget -qO- https://github.com/containernetworking/plugins/releases/download/$VERSION/cni-plugins-linux-amd64-$VERSION.tgz \
79+
| sudo tar xfz - -C /opt/cni/bin &&\
80+
ls -lah /opt/cni/bin
81+
- |
82+
VERSION=v1.0.0-rc8 &&\
83+
sudo wget -q -O \
84+
/usr/bin/runc \
85+
https://github.com/opencontainers/runc/releases/download/$VERSION/runc.amd64 &&\
86+
sudo chmod +x /usr/bin/runc &&\
87+
runc --version
88+
- sudo -E env "PATH=$PATH" make all install test-e2e TESTFLAGS=-v
6789

6890
stages:
6991
- Static check

Makefile

+10
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ $(GINKGO):
9595
release:
9696
hack/release.sh
9797

98+
# needs to run as root to work
99+
test-e2e: $(GINKGO)
100+
$(GINKGO) $(TESTFLAGS) \
101+
-r -p \
102+
--randomizeAllSpecs \
103+
--randomizeSuites \
104+
--succinct \
105+
test
106+
98107
vendor:
99108
export GO111MODULE=on \
100109
$(GO) mod tidy && \
@@ -116,4 +125,5 @@ vendor:
116125
lint \
117126
install.tools \
118127
release \
128+
test-e2e \
119129
vendor

test/e2e/help_test.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
Copyright 2017 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 e2e
18+
19+
import (
20+
. "github.com/onsi/ginkgo"
21+
)
22+
23+
// The actual test suite
24+
var _ = t.Describe("help", func() {
25+
26+
const helpMessageIdentifier = "crictl - client for CRI"
27+
28+
It("should succeed with `help` subcommand", func() {
29+
t.CrictlExpectSuccess("help", helpMessageIdentifier)
30+
})
31+
32+
It("should succeed with `--help` flag", func() {
33+
t.CrictlExpectSuccess("--help", helpMessageIdentifier)
34+
})
35+
36+
It("should succeed with `-h` flag", func() {
37+
t.CrictlExpectSuccess("-h", helpMessageIdentifier)
38+
})
39+
40+
It("should show help on invalid flag", func() {
41+
t.CrictlExpectFailure("--invalid", helpMessageIdentifier,
42+
"flag provided but not defined")
43+
})
44+
})

test/e2e/suite_test.go

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
Copyright 2017 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 e2e
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/kubernetes-sigs/cri-tools/test/framework"
23+
. "github.com/onsi/ginkgo"
24+
. "github.com/onsi/gomega"
25+
)
26+
27+
// TestE2E runs the created specs
28+
func TestE2E(t *testing.T) {
29+
RegisterFailHandler(Fail)
30+
RunSpecs(t, "e2e")
31+
}
32+
33+
var t *TestFramework
34+
35+
var _ = SynchronizedBeforeSuite(func() []byte {
36+
// Setup only once
37+
dir := SetupCrio()
38+
return []byte(dir)
39+
40+
}, func(dir []byte) {
41+
t = NewTestFramework()
42+
t.Setup(string(dir))
43+
})
44+
45+
var _ = AfterSuite(func() {
46+
t.Teardown()
47+
})

test/e2e/version_test.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
Copyright 2017 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 e2e
18+
19+
import (
20+
. "github.com/onsi/ginkgo"
21+
)
22+
23+
// The actual test suite
24+
var _ = t.Describe("version", func() {
25+
It("should succeed", func() {
26+
// Given
27+
endpoint, testDir, crio := t.StartCrio()
28+
29+
// When
30+
t.CrictlExpectSuccessWithEndpoint(endpoint, "version", "RuntimeName: cri-o")
31+
32+
// Then
33+
t.StopCrio(testDir, crio)
34+
})
35+
})

test/framework/framework.go

+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
Copyright 2017 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 framework
18+
19+
import (
20+
"fmt"
21+
"io/ioutil"
22+
"os"
23+
"os/exec"
24+
"path/filepath"
25+
"strings"
26+
"time"
27+
28+
. "github.com/onsi/ginkgo"
29+
. "github.com/onsi/gomega"
30+
. "github.com/onsi/gomega/gbytes"
31+
. "github.com/onsi/gomega/gexec"
32+
"github.com/sirupsen/logrus"
33+
)
34+
35+
// TestFramework is used to support commonly used test features
36+
type TestFramework struct {
37+
crioDir string
38+
}
39+
40+
// NewTestFramework creates a new test framework instance
41+
func NewTestFramework() *TestFramework {
42+
return &TestFramework{""}
43+
}
44+
45+
// Setup is the global initialization function which runs before each test
46+
// suite
47+
func (t *TestFramework) Setup(dir string) {
48+
// Global initialization for the whole framework goes in here
49+
logrus.SetLevel(logrus.DebugLevel)
50+
logrus.SetOutput(GinkgoWriter)
51+
t.crioDir = dir
52+
}
53+
54+
// Teardown is the global deinitialization function which runs after each test
55+
// suite
56+
func (t *TestFramework) Teardown() {
57+
}
58+
59+
// Describe is a convenience wrapper around the `ginkgo.Describe` function
60+
func (t *TestFramework) Describe(text string, body func()) bool {
61+
return Describe("crictl: "+text, body)
62+
}
63+
64+
// Convenience method for command creation
65+
func cmd(workDir, format string, args ...interface{}) *Session {
66+
c := strings.Split(fmt.Sprintf(format, args...), " ")
67+
command := exec.Command(c[0], c[1:]...)
68+
if workDir != "" {
69+
command.Dir = workDir
70+
}
71+
72+
session, err := Start(command, GinkgoWriter, GinkgoWriter)
73+
Expect(err).To(BeNil())
74+
75+
return session
76+
}
77+
78+
// Convenience method for command creation in the current working directory
79+
func lcmd(format string, args ...interface{}) *Session {
80+
return cmd("", format, args...)
81+
}
82+
83+
// Run crictl and return the resulting session
84+
func (t *TestFramework) Crictl(args string) *Session {
85+
return lcmd("crictl %s", args).Wait()
86+
}
87+
88+
// Run crictl on the specified endpoint and return the resulting session
89+
func (t *TestFramework) CrictlWithEndpoint(endpoint, args string) *Session {
90+
return lcmd("crictl --runtime-endpoint=%s %s", endpoint, args).Wait()
91+
}
92+
93+
// Run crictl and expect success containing the specified output
94+
func (t *TestFramework) CrictlExpectSuccess(args, expectedOut string) {
95+
t.CrictlExpectSuccessWithEndpoint("", args, expectedOut)
96+
}
97+
98+
// Run crictl and expect success containing the specified output
99+
func (t *TestFramework) CrictlExpectSuccessWithEndpoint(endpoint, args, expectedOut string) {
100+
// When
101+
res := t.CrictlWithEndpoint(endpoint, args)
102+
103+
// Then
104+
Expect(res).To(Exit(0))
105+
Expect(res.Out).To(Say(expectedOut))
106+
Expect(res.Err.Contents()).To(BeEmpty())
107+
}
108+
109+
// Run crictl and expect error containing the specified outputs
110+
func (t *TestFramework) CrictlExpectFailure(
111+
args string, expectedOut, expectedErr string,
112+
) {
113+
// When
114+
res := t.Crictl(args)
115+
116+
// Then
117+
Expect(res).To(Exit(1))
118+
Expect(res.Out).To(Say(expectedOut))
119+
Expect(res.Err).To(Say(expectedErr))
120+
}
121+
122+
func SetupCrio() string {
123+
const (
124+
crioURL = "https://github.com/cri-o/cri-o"
125+
timeout = 10 * time.Minute
126+
)
127+
tmpDir := filepath.Join(os.TempDir(), "crio-tmp")
128+
129+
if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
130+
logrus.Info("cloning and building CRI-O")
131+
lcmd("git clone --depth=1 %s %s", crioURL, tmpDir).Wait(timeout)
132+
cmd(tmpDir, "make").Wait(timeout)
133+
}
134+
135+
return tmpDir
136+
}
137+
138+
// Start the container runtime process
139+
func (t *TestFramework) StartCrio() (string, string, *Session) {
140+
// Create a new sandbox directory
141+
tmpDir, err := ioutil.TempDir("", "crictl-e2e-")
142+
Expect(err).To(BeNil())
143+
144+
// Copy everything together
145+
lcmd("cp -R %s %s", filepath.Join(t.crioDir, "bin"), tmpDir).Wait()
146+
147+
lcmd("cp %s %s", filepath.Join(t.crioDir, "test", "policy.json"),
148+
tmpDir).Wait()
149+
150+
lcmd("cp %s %s", filepath.Join(t.crioDir, "contrib", "cni",
151+
"10-crio-bridge.conf"), tmpDir).Wait()
152+
153+
for _, d := range []string{
154+
"cni-config", "root", "runroot", "log", "exits", "attach",
155+
} {
156+
Expect(os.MkdirAll(filepath.Join(tmpDir, d), 0755)).To(BeNil())
157+
}
158+
159+
endpoint := filepath.Join(tmpDir, "crio.sock")
160+
161+
session := cmd(tmpDir, "%s"+
162+
" --listen=%s"+
163+
" --conmon=%s"+
164+
" --container-exits-dir=%s"+
165+
" --container-attach-socket-dir=%s"+
166+
" --log-dir=%s"+
167+
" --signature-policy=%s"+
168+
" --cni-config-dir=%s"+
169+
" --root=%s"+
170+
" --runroot=%s"+
171+
" --storage-driver=vfs",
172+
filepath.Join(tmpDir, "bin", "crio"),
173+
endpoint,
174+
filepath.Join(tmpDir, "bin", "conmon"),
175+
filepath.Join(tmpDir, "exits"),
176+
filepath.Join(tmpDir, "attach"),
177+
filepath.Join(tmpDir, "log"),
178+
filepath.Join(tmpDir, "policy.json"),
179+
filepath.Join(tmpDir, "cni-config"),
180+
filepath.Join(tmpDir, "root"),
181+
filepath.Join(tmpDir, "runroot"),
182+
)
183+
184+
// Wait for the connection to be available
185+
for i := 0; i < 100; i++ {
186+
res := t.CrictlWithEndpoint(endpoint, "--timeout=200ms info")
187+
if res.ExitCode() == 0 {
188+
break
189+
}
190+
logrus.Info("waiting for CRI-O to become ready")
191+
}
192+
return endpoint, tmpDir, session
193+
}
194+
195+
// Stop the container runtime process
196+
func (t *TestFramework) StopCrio(testDir string, session *Session) {
197+
Expect(session.Interrupt().Wait()).To(Exit(0))
198+
Expect(os.RemoveAll(testDir)).To(BeNil())
199+
}

0 commit comments

Comments
 (0)