Skip to content

Commit 242968f

Browse files
authored
Merge pull request #97 from kluctl/feat-tests
Add controller tests and CI
2 parents 22d4ffb + f0f5e34 commit 242968f

19 files changed

+570
-97
lines changed

.github/workflows/tests.yml

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
name: tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- release-v*
8+
pull_request:
9+
branches:
10+
- main
11+
12+
jobs:
13+
generate-checks:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
with:
19+
fetch-depth: 0
20+
- uses: actions/setup-go@v5
21+
with:
22+
go-version-file: go.mod
23+
- uses: actions/cache@v4
24+
with:
25+
path: |
26+
~/go/pkg/mod
27+
~/.cache/go-build
28+
key: generate-check-go-${{ runner.os }}-${{ hashFiles('**/go.sum') }}
29+
restore-keys: |
30+
generate-check-go-${{ runner.os }}-
31+
- name: Verify generated source is up-to-date
32+
run: |
33+
make generate
34+
if [ ! -z "$(git status --porcelain)" ]; then
35+
echo "make generate must be invoked and the result committed"
36+
git status
37+
git diff
38+
exit 1
39+
fi
40+
- name: Verify generated manifests are up-to-date
41+
run: |
42+
make manifests
43+
if [ ! -z "$(git status --porcelain)" ]; then
44+
echo "make manifests must be invoked and the result committed"
45+
git status
46+
git diff
47+
exit 1
48+
fi
49+
- name: Verify generated api-docs are up-to-date
50+
run: |
51+
make api-docs
52+
if [ ! -z "$(git status --porcelain)" ]; then
53+
echo "make api-docs must be invoked and the result committed"
54+
git status
55+
git diff
56+
exit 1
57+
fi
58+
- name: Verify go.mod and go.sum are clean
59+
run: |
60+
go mod tidy
61+
if [ ! -z "$(git status --porcelain)" ]; then
62+
echo "go mod tidy must be invoked and the result committed"
63+
git status
64+
git diff
65+
exit 1
66+
fi
67+
68+
tests:
69+
runs-on: ubuntu-22.04
70+
name: tests
71+
steps:
72+
- name: Checkout
73+
uses: actions/checkout@v4
74+
- uses: actions/setup-go@v5
75+
with:
76+
go-version-file: go.mod
77+
- uses: actions/setup-python@v5
78+
with:
79+
python-version: '3.11'
80+
- uses: actions/cache@v4
81+
with:
82+
path: |
83+
~/go/pkg/mod
84+
~/.cache/go-build
85+
key: tests-go-${{ runner.os }}-${{ hashFiles('**/go.sum') }}
86+
restore-keys: |
87+
tests-go-${{ runner.os }}-
88+
- name: Run tests
89+
shell: bash
90+
run: |
91+
make test

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ vet: ## Run go vet against code.
5656

5757
.PHONY: test
5858
test: manifests generate fmt vet envtest ## Run tests.
59-
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out
59+
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out --ginkgo.v
6060

6161
##@ Build
6262

api/v1alpha1/objecttemplate_types.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ type ObjectTemplateSpec struct {
5050

5151
// Matrix specifies the input matrix
5252
// +required
53-
Matrix []*MatrixEntry `json:"matrix"`
53+
Matrix []MatrixEntry `json:"matrix"`
5454

5555
// Templates specifies a list of templates to render and deploy
5656
// +required

api/v1alpha1/texttemplate_types.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ type TextTemplateSpec struct {
3333
ServiceAccountName string `json:"serviceAccountName,omitempty"`
3434

3535
// +optional
36-
Inputs []*TextTemplateInput `json:"inputs,omitempty"`
36+
Inputs []TextTemplateInput `json:"inputs,omitempty"`
3737

3838
// +optional
3939
Template *string `json:"template,omitempty"`

api/v1alpha1/zz_generated.deepcopy.go

+4-12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/templates.kluctl.io_githubcomments.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.14.0
6+
controller-gen.kubebuilder.io/version: v0.15.0
77
name: githubcomments.templates.kluctl.io
88
spec:
99
group: templates.kluctl.io

config/crd/bases/templates.kluctl.io_gitlabcomments.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.14.0
6+
controller-gen.kubebuilder.io/version: v0.15.0
77
name: gitlabcomments.templates.kluctl.io
88
spec:
99
group: templates.kluctl.io

config/crd/bases/templates.kluctl.io_gitprojectors.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.14.0
6+
controller-gen.kubebuilder.io/version: v0.15.0
77
name: gitprojectors.templates.kluctl.io
88
spec:
99
group: templates.kluctl.io

config/crd/bases/templates.kluctl.io_listgithubpullrequests.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.14.0
6+
controller-gen.kubebuilder.io/version: v0.15.0
77
name: listgithubpullrequests.templates.kluctl.io
88
spec:
99
group: templates.kluctl.io

config/crd/bases/templates.kluctl.io_listgitlabmergerequests.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.14.0
6+
controller-gen.kubebuilder.io/version: v0.15.0
77
name: listgitlabmergerequests.templates.kluctl.io
88
spec:
99
group: templates.kluctl.io

config/crd/bases/templates.kluctl.io_objecttemplates.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.14.0
6+
controller-gen.kubebuilder.io/version: v0.15.0
77
name: objecttemplates.templates.kluctl.io
88
spec:
99
group: templates.kluctl.io

config/crd/bases/templates.kluctl.io_texttemplates.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.14.0
6+
controller-gen.kubebuilder.io/version: v0.15.0
77
name: texttemplates.templates.kluctl.io
88
spec:
99
group: templates.kluctl.io

controllers/base_template_reconciler.go

+1-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"k8s.io/apimachinery/pkg/types"
1212
"k8s.io/client-go/rest"
1313
"sigs.k8s.io/controller-runtime/pkg/client"
14-
"sigs.k8s.io/controller-runtime/pkg/client/config"
1514
"sigs.k8s.io/controller-runtime/pkg/controller"
1615
"sigs.k8s.io/controller-runtime/pkg/handler"
1716
"sigs.k8s.io/controller-runtime/pkg/log"
@@ -33,10 +32,7 @@ type BaseTemplateReconciler struct {
3332
}
3433

3534
func (r *BaseTemplateReconciler) getClientForObjects(serviceAccountName string, objNamespace string) (client.Client, error) {
36-
restConfig, err := config.GetConfig()
37-
if err != nil {
38-
return nil, err
39-
}
35+
restConfig := rest.CopyConfig(r.Manager.GetConfig())
4036

4137
name := "default"
4238
if serviceAccountName != "" {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
Copyright 2022.
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 controllers
18+
19+
import (
20+
"fmt"
21+
v1 "k8s.io/api/core/v1"
22+
"math/rand/v2"
23+
"sigs.k8s.io/controller-runtime/pkg/client"
24+
"time"
25+
26+
templatesv1alpha1 "github.com/kluctl/template-controller/api/v1alpha1"
27+
. "github.com/onsi/ginkgo/v2"
28+
. "github.com/onsi/gomega"
29+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
)
31+
32+
var _ = Describe("ObjectTemplate controller", func() {
33+
const (
34+
timeout = time.Second * 1000
35+
duration = time.Second * 10
36+
interval = time.Millisecond * 250
37+
)
38+
39+
Context("Template without permissions to write object", func() {
40+
ns := fmt.Sprintf("test-%d", rand.Int64())
41+
42+
key := client.ObjectKey{Name: "t1", Namespace: ns}
43+
cmKey := client.ObjectKey{Name: "cm1", Namespace: ns}
44+
45+
t := buildObjectTemplate(key.Name, key.Namespace,
46+
[]templatesv1alpha1.MatrixEntry{buildMatrixListEntry("m1")},
47+
[]templatesv1alpha1.Template{
48+
{Object: buildTestConfigMap(cmKey.Name, cmKey.Namespace, map[string]string{
49+
"k1": `{{ matrix.m1.k1 + matrix.m1.k2 }}`,
50+
})},
51+
})
52+
53+
It("Should fail initially", func() {
54+
createNamespace(ns)
55+
createServiceAccount("default", ns)
56+
57+
Expect(k8sClient.Create(ctx, t)).Should(Succeed())
58+
waitUntiReconciled(key, timeout)
59+
60+
Consistently(func() error {
61+
return k8sClient.Get(ctx, cmKey, &v1.ConfigMap{})
62+
}, "2s").Should(MatchError("configmaps \"cm1\" not found"))
63+
64+
assertFailedConfigMaps(key, cmKey)
65+
})
66+
It("Should succeed when RBAC is added", func() {
67+
createRoleWithBinding("default", ns, []string{"configmaps"})
68+
69+
triggerReconcile(key)
70+
waitUntiReconciled(key, timeout)
71+
72+
assertAppliedConfigMaps(key, cmKey)
73+
74+
var cm v1.ConfigMap
75+
err := k8sClient.Get(ctx, cmKey, &cm)
76+
Expect(err).To(Succeed())
77+
78+
Expect(cm.Data).To(Equal(map[string]string{
79+
"k1": "3",
80+
}))
81+
})
82+
It("Should fail with non-existing SA", func() {
83+
updateObjectTemplate(key, func(t *templatesv1alpha1.ObjectTemplate) {
84+
t.Spec.ServiceAccountName = "non-existent"
85+
})
86+
waitUntiReconciled(key, timeout)
87+
88+
assertFailedConfigMap(key, cmKey, "configmaps \"cm1\" is forbidden")
89+
})
90+
It("Should succeed after the SA is being created", func() {
91+
createServiceAccount("non-existent", ns)
92+
createRoleWithBinding("non-existent", ns, []string{"configmaps"})
93+
triggerReconcile(key)
94+
waitUntiReconciled(key, timeout)
95+
assertAppliedConfigMaps(key, cmKey)
96+
})
97+
})
98+
Context("Template without permissions to read matrix object", func() {
99+
ns := fmt.Sprintf("test-%d", rand.Int64())
100+
101+
key := client.ObjectKey{Name: "t1", Namespace: ns}
102+
cmKey := client.ObjectKey{Name: "cm1", Namespace: ns}
103+
104+
It("Should fail initially", func() {
105+
createNamespace(ns)
106+
createServiceAccount("default", ns)
107+
createRoleWithBinding("default", ns, []string{"configmaps"})
108+
109+
t := buildObjectTemplate(key.Name, key.Namespace,
110+
[]templatesv1alpha1.MatrixEntry{
111+
buildMatrixObjectEntry("m1", "m1", ns, "Secret", "", false),
112+
},
113+
[]templatesv1alpha1.Template{
114+
{Object: buildTestConfigMap(cmKey.Name, cmKey.Namespace, map[string]string{
115+
"k1": `{{ matrix.m1.k1 + matrix.m1.k2 }}`,
116+
})},
117+
})
118+
119+
err := k8sClient.Create(ctx, buildTestSecret("m1", ns, map[string]string{
120+
"k1": "1",
121+
"k2": "2",
122+
}))
123+
Expect(err).To(Succeed())
124+
125+
Expect(k8sClient.Create(ctx, t)).Should(Succeed())
126+
waitUntiReconciled(key, timeout)
127+
128+
t2 := getObjectTemplate(key)
129+
c := getReadyCondition(t2.GetConditions())
130+
Expect(c.Status).To(Equal(metav1.ConditionFalse))
131+
Expect(c.Message).To(ContainSubstring("secrets \"m1\" is forbidden"))
132+
})
133+
It("Should succeed when RBAC is created", func() {
134+
createRoleWithBinding("default", ns, []string{"secrets"})
135+
triggerReconcile(key)
136+
waitUntiReconciled(key, timeout)
137+
assertAppliedConfigMaps(key, cmKey)
138+
})
139+
It("Should fail with non-existing SA", func() {
140+
updateObjectTemplate(key, func(t *templatesv1alpha1.ObjectTemplate) {
141+
t.Spec.ServiceAccountName = "non-existent"
142+
})
143+
waitUntiReconciled(key, timeout)
144+
145+
t2 := getObjectTemplate(key)
146+
c := getReadyCondition(t2.GetConditions())
147+
Expect(c.Status).To(Equal(metav1.ConditionFalse))
148+
Expect(c.Message).To(ContainSubstring("secrets \"m1\" is forbidden"))
149+
})
150+
It("Should succeed after the SA is being created", func() {
151+
createServiceAccount("non-existent", ns)
152+
createRoleWithBinding("non-existent", ns, []string{"configmaps"})
153+
triggerReconcile(key)
154+
waitUntiReconciled(key, timeout)
155+
assertAppliedConfigMaps(key, cmKey)
156+
})
157+
})
158+
})

0 commit comments

Comments
 (0)