Skip to content

Commit e38447a

Browse files
authored
write basic integration tests using a local kind cluster (#150)
1 parent 1768c02 commit e38447a

File tree

12 files changed

+358
-31
lines changed

12 files changed

+358
-31
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,14 @@ jobs:
2727
echo "::add-path::$HOME/go/bin"
2828
make install-ci install
2929
30-
- name: Install package dependencies
31-
run: make get
32-
3330
- name: Build
3431
run: make build
3532

3633
- name: Lint
3734
run: make lint
3835

3936
- name: Test
40-
run: make test
37+
run: TEST_FLAGS="-tags integration" make test
4138

4239
- name: Coverage Report
4340
run: make publish-coverage

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ helm.tar.gz
99
linux-amd64/
1010
bin/
1111
.release-notes.md
12+
.tools/

.golangci.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
run:
2+
timeout: 3m
3+
tests: true
4+
5+
linters-settings:
6+
exhaustive:
7+
default-signifies-exhaustive: false
8+
golint:
9+
min-confidence: 0
10+
11+
linters:
12+
disable-all: true
13+
enable:
14+
- bodyclose
15+
- deadcode
16+
- depguard
17+
- dogsled
18+
- errcheck
19+
- goconst
20+
- gocyclo
21+
- gofmt
22+
- goimports
23+
- golint
24+
- gosimple
25+
- govet
26+
- ineffassign
27+
- staticcheck
28+
- structcheck
29+
- stylecheck
30+
- typecheck
31+
- unconvert
32+
- unused
33+
- varcheck

Makefile

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
include Makefile.tools
2+
13
VERSION := 0.12.1
24
SHORT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo dev)
35
GO_VERSION := $(shell go version | awk '{ print $$3}' | sed 's/^go//')
@@ -11,25 +13,24 @@ LD_FLAGS += -X "$(LD_FLAGS_PKG).commit=$(SHORT_COMMIT)"
1113
LD_FLAGS += -X "$(LD_FLAGS_PKG).goVersion=$(GO_VERSION)"
1214

1315
LINT_FLAGS ?=
16+
TEST_FLAGS ?=
1417

1518
export GO111MODULE=on
1619
GOOS ?= $(shell go env GOOS)
1720
GOARCH ?= $(shell go env GOARCH)
1821

19-
.PHONY: all
20-
all: get build lint test
22+
.DEFAULT_GOAL := all
2123

22-
.PHONY: get
23-
get:
24-
go get ./...
24+
.PHONY: all
25+
all: build lint test
2526

2627
.PHONY: build
2728
build:
2829
go install -ldflags '$(LD_FLAGS)' ./...
2930

3031
.PHONY: test
3132
test:
32-
go test -coverprofile=coverage.txt -covermode=atomic -race ./...
33+
go test $(TEST_FLAGS) -coverprofile=coverage.txt -covermode=atomic -race ./...
3334

3435
.PHONY: publish-coverage
3536
publish-coverage:
@@ -52,11 +53,12 @@ check-format:
5253
@echo All files are well formatted.\
5354
)
5455
.PHONY: install-ci
55-
install-ci:
56+
install-ci: .tools/kind
5657
curl -sSL -o helm.tar.gz https://storage.googleapis.com/kubernetes-helm/helm-v2.13.1-linux-amd64.tar.gz
5758
tar -xvzf helm.tar.gz
5859
mv linux-amd64/helm $(GOPATH)/bin/
5960
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOPATH)/bin v1.21.0
61+
.tools/kind create cluster
6062

6163
.PHONY: install
6264
install:

Makefile.tools

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
.tools/jb: JB_VERSION := v0.4.0
3+
.tools/jb: JB_PLATFORM := $(shell uname | tr '[:upper:]' '[:lower:]')
4+
.tools/jb:
5+
mkdir -p .tools
6+
curl -sSL -o .tools/jb https://github.com/jsonnet-bundler/jsonnet-bundler/releases/download/$(JB_VERSION)/jb-$(JB_PLATFORM)-amd64
7+
chmod +x .tools/jb
8+
9+
.tools/kind: KIND_VERSION := 0.7.0
10+
.tools/kind: KIND_PLATFORM := $(shell uname)
11+
.tools/kind:
12+
mkdir -p .tools
13+
curl -o $@ -sSL https://kind.sigs.k8s.io/dl/v0.7.0/kind-$(KIND_PLATFORM)-amd64
14+
chmod +x $@
15+
16+
tools: .tools/jb .tools/kind
17+

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ require (
3838
github.com/stretchr/testify v1.4.0
3939
gopkg.in/inf.v0 v0.9.1 // indirect
4040
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
41+
k8s.io/api v0.0.0-20191016110246-af539daaa43a
4142
k8s.io/apimachinery v0.0.0-20191004115701-31ade1b30762
4243
k8s.io/client-go v0.0.0-20191016110837-54936ba21026
4344
k8s.io/gengo v0.0.0-20200728071708-7794989d0000
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// +build integration
2+
3+
package commands
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"sync"
9+
"testing"
10+
11+
"github.com/stretchr/testify/require"
12+
v1 "k8s.io/api/core/v1"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
15+
"k8s.io/client-go/rest"
16+
"k8s.io/client-go/tools/clientcmd"
17+
)
18+
19+
var (
20+
once sync.Once
21+
contextName string
22+
restConfig *rest.Config
23+
coreClient *corev1.CoreV1Client
24+
)
25+
26+
func initialize(t *testing.T) {
27+
contextName = os.Getenv("QBEC_CONTEXT")
28+
if contextName != "" {
29+
contextName = "kind-kind"
30+
}
31+
c, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
32+
clientcmd.NewDefaultClientConfigLoadingRules(),
33+
&clientcmd.ConfigOverrides{CurrentContext: contextName},
34+
).ClientConfig()
35+
require.NoError(t, err)
36+
restConfig = c
37+
coreClient, err = corev1.NewForConfig(restConfig)
38+
require.NoError(t, err)
39+
}
40+
41+
type integrationScaffold struct {
42+
baseScaffold
43+
ns string
44+
}
45+
46+
func newNamespace(t *testing.T) (name string, reset func()) {
47+
once.Do(func() { initialize(t) })
48+
ns, err := coreClient.Namespaces().Create(&v1.Namespace{
49+
ObjectMeta: metav1.ObjectMeta{
50+
GenerateName: "qbec-test-",
51+
},
52+
Spec: v1.NamespaceSpec{},
53+
})
54+
require.NoError(t, err)
55+
return ns.GetName(), func() {
56+
pp := metav1.DeletePropagationForeground
57+
err := coreClient.Namespaces().Delete(ns.GetName(), &metav1.DeleteOptions{PropagationPolicy: &pp})
58+
if err != nil {
59+
fmt.Println("Error deleting namespace", ns.GetName(), err)
60+
}
61+
}
62+
}
63+
64+
func newIntegrationScaffold(t *testing.T, ns string, dir string) *integrationScaffold {
65+
once.Do(func() { initialize(t) })
66+
b := newBaseScaffold(t, dir, nil)
67+
return &integrationScaffold{
68+
baseScaffold: b,
69+
ns: ns,
70+
}
71+
}
72+
73+
func (s *integrationScaffold) executeCommand(testArgs ...string) error {
74+
args := append(testArgs,
75+
"--force:k8s-context="+contextName,
76+
"--force:k8s-namespace="+s.ns,
77+
)
78+
return s.baseScaffold.executeCommand(args...)
79+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// +build integration
2+
3+
package commands
4+
5+
import (
6+
"regexp"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestIntegrationBasic(t *testing.T) {
14+
dir := "testdata/projects/simple-service"
15+
ns, reset := newNamespace(t)
16+
defer reset()
17+
18+
t.Run("show", func(t *testing.T) {
19+
s := newIntegrationScaffold(t, ns, dir)
20+
defer s.reset()
21+
err := s.executeCommand("show", "-O", "local")
22+
require.NoError(t, err)
23+
s.assertOutputLineMatch(regexp.MustCompile(`^simple-service\s+ConfigMap\s+nginx`))
24+
s.assertOutputLineMatch(regexp.MustCompile(`^simple-service\s+Service\s+nginx`))
25+
s.assertOutputLineMatch(regexp.MustCompile(`^simple-service\s+Deployment\s+nginx`))
26+
})
27+
t.Run("validate", func(t *testing.T) {
28+
s := newIntegrationScaffold(t, ns, dir)
29+
defer s.reset()
30+
err := s.executeCommand("validate", "local")
31+
require.NoError(t, err)
32+
s.assertOutputLineMatch(regexp.MustCompile(`✔ configmaps nginx`))
33+
s.assertOutputLineMatch(regexp.MustCompile(`✔ deployments nginx`))
34+
s.assertOutputLineMatch(regexp.MustCompile(`✔ services nginx`))
35+
})
36+
allAddsDiffTest := func(t *testing.T) {
37+
s := newIntegrationScaffold(t, ns, dir)
38+
defer s.reset()
39+
err := s.executeCommand("diff", "--error-exit=false", "local")
40+
require.NoError(t, err)
41+
stats := s.outputStats()
42+
a := assert.New(t)
43+
a.EqualValues(3, len(stats["additions"].([]interface{})))
44+
}
45+
t.Run("diff", allAddsDiffTest)
46+
t.Run("apply dryrun", func(t *testing.T) {
47+
s := newIntegrationScaffold(t, ns, dir)
48+
defer s.reset()
49+
err := s.executeCommand("apply", "-n", "local")
50+
require.NoError(t, err)
51+
stats := s.outputStats()
52+
a := assert.New(t)
53+
a.EqualValues(3, len(stats["created"].([]interface{})))
54+
})
55+
// ensure dryrun did not change state above
56+
t.Run("diff1", allAddsDiffTest)
57+
t.Run("apply", func(t *testing.T) {
58+
s := newIntegrationScaffold(t, ns, dir)
59+
defer s.reset()
60+
err := s.executeCommand("apply", "local", "--wait")
61+
require.NoError(t, err)
62+
stats := s.outputStats()
63+
a := assert.New(t)
64+
a.EqualValues(3, len(stats["created"].([]interface{})))
65+
})
66+
t.Run("diff2", func(t *testing.T) {
67+
s := newIntegrationScaffold(t, ns, dir)
68+
defer s.reset()
69+
err := s.executeCommand("diff", "--error-exit=false", "local")
70+
require.NoError(t, err)
71+
stats := s.outputStats()
72+
a := assert.New(t)
73+
a.EqualValues(3, stats["same"])
74+
})
75+
76+
changeArgs := []string{"--vm:ext-code=replicas=2", "--vm:ext-str=cmContent=goodbye world"}
77+
t.Run("diff3", func(t *testing.T) {
78+
s := newIntegrationScaffold(t, ns, dir)
79+
defer s.reset()
80+
err := s.executeCommand(append(changeArgs, "diff", "--error-exit=false", "local")...)
81+
require.NoError(t, err)
82+
stats := s.outputStats()
83+
a := assert.New(t)
84+
a.EqualValues(1, stats["same"])
85+
a.EqualValues(2, len(stats["changes"].([]interface{})))
86+
s.assertOutputLineMatch(regexp.MustCompile(`-\s+replicas: 1$`))
87+
s.assertOutputLineMatch(regexp.MustCompile(`\+\s+replicas: 2$`))
88+
s.assertOutputLineMatch(regexp.MustCompile(`-\s+index.html: hello world$`))
89+
s.assertOutputLineMatch(regexp.MustCompile(`\+\s+index.html: goodbye world$`))
90+
})
91+
t.Run("apply2", func(t *testing.T) {
92+
s := newIntegrationScaffold(t, ns, dir)
93+
defer s.reset()
94+
err := s.executeCommand(append(changeArgs, "apply", "local", "--wait")...)
95+
require.NoError(t, err)
96+
stats := s.outputStats()
97+
a := assert.New(t)
98+
a.EqualValues(1, stats["same"])
99+
a.EqualValues(2, len(stats["updated"].([]interface{})))
100+
})
101+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
apiVersion: v1
3+
kind: ConfigMap
4+
metadata:
5+
name: nginx
6+
data:
7+
'index.html': 'Hello world'
8+
---
9+
apiVersion: apps/v1
10+
kind: Deployment
11+
metadata:
12+
labels:
13+
app: nginx
14+
name: nginx
15+
spec:
16+
progressDeadlineSeconds: 600
17+
replicas: 1
18+
revisionHistoryLimit: 10
19+
selector:
20+
matchLabels:
21+
app: nginx
22+
strategy:
23+
rollingUpdate:
24+
maxSurge: 25%
25+
maxUnavailable: 25%
26+
type: RollingUpdate
27+
template:
28+
metadata:
29+
labels:
30+
app: nginx
31+
spec:
32+
containers:
33+
- image: nginx
34+
imagePullPolicy: Always
35+
name: nginx
36+
volumeMounts:
37+
- mountPath: /usr/share/nginx/html
38+
name: content
39+
volumes:
40+
- configMap:
41+
name: nginx
42+
name: content
43+
---
44+
apiVersion: v1
45+
kind: Service
46+
metadata:
47+
labels:
48+
app: nginx
49+
name: nginx
50+
spec:
51+
ports:
52+
- name: http
53+
port: 80
54+
protocol: TCP
55+
selector:
56+
app: nginx
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
function(object) (
2+
// please don't do things like this for real. This is just to make my testing easy :)
3+
local replicas = std.extVar('replicas');
4+
local cmContent = std.extVar('cmContent');
5+
if object.kind == 'Deployment' then
6+
object { spec+: { replicas: replicas } }
7+
else if object.kind == 'ConfigMap' then
8+
object { data+: { 'index.html': cmContent } }
9+
else
10+
object
11+
)

0 commit comments

Comments
 (0)