Skip to content

Commit 02062fa

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 02062fa

File tree

14 files changed

+1284
-1
lines changed

14 files changed

+1284
-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,8 +65,29 @@ 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: e2e
69+
os: linux
70+
script:
71+
- |
72+
sudo apt-get update &&\
73+
sudo apt-get install -y libseccomp-dev
74+
- |
75+
VERSION=v0.8.2 &&\
76+
sudo mkdir -p /opt/cni/bin &&\
77+
sudo wget -qO- https://github.com/containernetworking/plugins/releases/download/$VERSION/cni-plugins-linux-amd64-$VERSION.tgz \
78+
| sudo tar xfz - -C /opt/cni/bin &&\
79+
ls -lah /opt/cni/bin
80+
- |
81+
VERSION=v1.0.0-rc8 &&\
82+
sudo wget -q -O \
83+
/usr/bin/runc \
84+
https://github.com/opencontainers/runc/releases/download/$VERSION/runc.amd64 &&\
85+
sudo chmod +x /usr/bin/runc &&\
86+
runc --version
87+
- sudo -E env "PATH=$PATH" make all install test-e2e TESTFLAGS=-v
6788

6889
stages:
90+
- e2e
6991
- Static check
7092
- Build
7193
- Test

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

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package e2e
2+
3+
import (
4+
. "github.com/onsi/ginkgo"
5+
)
6+
7+
// The actual test suite
8+
var _ = t.Describe("help", func() {
9+
10+
const helpMessageIdentifier = "crictl - client for CRI"
11+
12+
It("should succeed with `help` subcommand", func() {
13+
t.CrictlExpectSuccess("help", helpMessageIdentifier)
14+
})
15+
16+
It("should succeed with `--help` flag", func() {
17+
t.CrictlExpectSuccess("--help", helpMessageIdentifier)
18+
})
19+
20+
It("should succeed with `-h` flag", func() {
21+
t.CrictlExpectSuccess("-h", helpMessageIdentifier)
22+
})
23+
24+
It("should show help on invalid flag", func() {
25+
t.CrictlExpectFailure("--invalid", helpMessageIdentifier,
26+
"flag provided but not defined")
27+
})
28+
})

test/e2e/suite_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package e2e
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/kubernetes-sigs/cri-tools/test/framework"
7+
. "github.com/onsi/ginkgo"
8+
. "github.com/onsi/gomega"
9+
)
10+
11+
// TestE2E runs the created specs
12+
func TestE2E(t *testing.T) {
13+
RegisterFailHandler(Fail)
14+
RunSpecs(t, "e2e")
15+
}
16+
17+
var t *TestFramework
18+
19+
var _ = SynchronizedBeforeSuite(func() []byte {
20+
// Setup only once
21+
dir := SetupCrio()
22+
return []byte(dir)
23+
24+
}, func(dir []byte) {
25+
t = NewTestFramework()
26+
t.Setup(string(dir))
27+
})
28+
29+
var _ = AfterSuite(func() {
30+
t.Teardown()
31+
})

test/e2e/version_test.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package e2e
2+
3+
import (
4+
. "github.com/onsi/ginkgo"
5+
)
6+
7+
// The actual test suite
8+
var _ = t.Describe("version", func() {
9+
It("should succeed", func() {
10+
// Given
11+
endpoint, testDir, crio := t.StartCrio()
12+
13+
// When
14+
t.CrictlExpectSuccessWithEndpoint(endpoint, "version", "RuntimeName: cri-o")
15+
16+
// Then
17+
t.StopCrio(testDir, crio)
18+
})
19+
})

test/framework/framework.go

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

0 commit comments

Comments
 (0)