Skip to content

Commit 1f767a9

Browse files
committed
Add tests for provisioning Kubernetes components
Signed-off-by: Angel Misevski <[email protected]>
1 parent 4043db3 commit 1f767a9

11 files changed

+814
-22
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) 2019-2022 Red Hat, Inc.
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package kubernetes
15+
16+
import (
17+
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
18+
"github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
19+
"github.com/devfile/devworkspace-operator/pkg/provision/sync"
20+
"k8s.io/apimachinery/pkg/runtime"
21+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
22+
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
23+
)
24+
25+
var (
26+
testScheme = runtime.NewScheme()
27+
testAPI = sync.ClusterAPI{
28+
Scheme: testScheme,
29+
}
30+
)
31+
32+
func init() {
33+
utilruntime.Must(clientgoscheme.AddToScheme(testScheme))
34+
utilruntime.Must(v1alpha1.AddToScheme(testScheme))
35+
utilruntime.Must(dw.AddToScheme(testScheme))
36+
}

pkg/library/kubernetes/deserialize_test.go

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,34 +18,17 @@ import (
1818
"os"
1919
"testing"
2020

21-
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
22-
"github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
23-
"github.com/devfile/devworkspace-operator/pkg/provision/sync"
2421
"github.com/google/go-cmp/cmp"
2522
"github.com/stretchr/testify/assert"
2623
corev1 "k8s.io/api/core/v1"
27-
"k8s.io/apimachinery/pkg/runtime"
28-
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
29-
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
3024
"sigs.k8s.io/controller-runtime/pkg/client"
3125
"sigs.k8s.io/yaml"
3226
)
3327

34-
var (
35-
scheme = runtime.NewScheme()
36-
api = sync.ClusterAPI{
37-
Scheme: scheme,
38-
}
39-
)
40-
41-
func init() {
42-
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
43-
utilruntime.Must(v1alpha1.AddToScheme(scheme))
44-
utilruntime.Must(dw.AddToScheme(scheme))
45-
}
46-
4728
func TestDeserializeObject(t *testing.T) {
48-
InitializeDeserializer(scheme)
29+
if err := InitializeDeserializer(testScheme); err != nil {
30+
t.Fatalf("Unexpected error: %s", err)
31+
}
4932
defer func() {
5033
decoder = nil
5134
}()
@@ -89,7 +72,7 @@ func TestDeserializeObject(t *testing.T) {
8972
for _, tt := range tests {
9073
t.Run(fmt.Sprintf("%s (%s)", tt.name, tt.filePath), func(t *testing.T) {
9174
jsonBytes := readBytesFromFile(t, tt.filePath)
92-
actualObj, err := deserializeToObject(jsonBytes, api)
75+
actualObj, err := deserializeToObject(jsonBytes, testAPI)
9376
if tt.expectedErrRegexp != "" {
9477
if !assert.Error(t, err, "Expect error to be returned") {
9578
return
@@ -108,7 +91,7 @@ func TestDeserializeObject(t *testing.T) {
10891
}
10992

11093
func TestErrorIfDeserializerNotInitialized(t *testing.T) {
111-
_, err := deserializeToObject([]byte(""), api)
94+
_, err := deserializeToObject([]byte(""), testAPI)
11295
assert.Error(t, err)
11396
assert.Equal(t, "kubernetes object deserializer is not initialized", err.Error())
11497
}
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
// Copyright (c) 2019-2022 Red Hat, Inc.
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package kubernetes
15+
16+
import (
17+
"errors"
18+
"fmt"
19+
"os"
20+
"path/filepath"
21+
"reflect"
22+
"testing"
23+
24+
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
25+
"github.com/devfile/devworkspace-operator/pkg/common"
26+
"github.com/devfile/devworkspace-operator/pkg/constants"
27+
"github.com/devfile/devworkspace-operator/pkg/provision/sync"
28+
testlog "github.com/go-logr/logr/testing"
29+
"github.com/google/go-cmp/cmp"
30+
"github.com/google/go-cmp/cmp/cmpopts"
31+
"github.com/stretchr/testify/assert"
32+
corev1 "k8s.io/api/core/v1"
33+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34+
"k8s.io/apimachinery/pkg/types"
35+
"k8s.io/utils/pointer"
36+
"sigs.k8s.io/controller-runtime/pkg/client"
37+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
38+
"sigs.k8s.io/yaml"
39+
)
40+
41+
type testCase struct {
42+
Name string `json:"name,omitempty"`
43+
Input testInput `json:"input,omitempty"`
44+
Output testOutput `json:"output,omitempty"`
45+
originalFilename string
46+
}
47+
48+
type testInput struct {
49+
Components []dw.Component `json:"components,omitempty"`
50+
ExistingObjects clusterObjects `json:"existingObjects,omitempty"`
51+
}
52+
53+
type testOutput struct {
54+
ExpectedObjects clusterObjects `json:"expectedObjects,omitempty"`
55+
ErrRegexp *string `json:"errRegexp,omitempty"`
56+
}
57+
58+
type clusterObjects struct {
59+
Pods []corev1.Pod `json:"pods,omitempty"`
60+
Services []corev1.Service `json:"services,omitempty"`
61+
}
62+
63+
const (
64+
testID = "test-devworkspaceID"
65+
testCreatorID = "test-creatorID"
66+
testDevWorkspaceName = "test-devworkspace"
67+
testDevWorkspaceUID = "test-UID"
68+
testNamespace = "test-devworkspace"
69+
)
70+
71+
var testDevWorkspace = &dw.DevWorkspace{
72+
ObjectMeta: metav1.ObjectMeta{
73+
Name: testDevWorkspaceName,
74+
Namespace: testNamespace,
75+
Labels: map[string]string{
76+
constants.DevWorkspaceCreatorLabel: testCreatorID,
77+
},
78+
UID: testDevWorkspaceUID,
79+
},
80+
Spec: dw.DevWorkspaceSpec{
81+
Template: dw.DevWorkspaceTemplateSpec{
82+
DevWorkspaceTemplateSpecContent: dw.DevWorkspaceTemplateSpecContent{},
83+
},
84+
},
85+
Status: dw.DevWorkspaceStatus{
86+
DevWorkspaceId: testID,
87+
},
88+
}
89+
90+
func TestHandleKubernetesComponents(t *testing.T) {
91+
if err := InitializeDeserializer(testScheme); err != nil {
92+
t.Fatalf("Unexpected error: %s", err)
93+
}
94+
defer func() { decoder = nil }()
95+
tests := loadAllTestCasesOrPanic(t, "testdata/provision_tests")
96+
for _, tt := range tests {
97+
t.Run(fmt.Sprintf("%s (%s)", tt.Name, tt.originalFilename), func(t *testing.T) {
98+
testClient := fake.NewClientBuilder().WithScheme(testScheme).WithObjects(collectClusterObj(tt.Input.ExistingObjects)...).Build()
99+
api := sync.ClusterAPI{
100+
Client: testClient,
101+
Scheme: testScheme,
102+
Logger: testlog.TestLogger{T: t},
103+
}
104+
wksp := &common.DevWorkspaceWithConfig{
105+
DevWorkspace: testDevWorkspace.DeepCopy(),
106+
}
107+
wksp.Spec.Template.Components = append(wksp.Spec.Template.Components, tt.Input.Components...)
108+
// Repeat function as long as it returns RetryError
109+
i := 0
110+
maxIters := 30
111+
var err error
112+
retryErr := &RetryError{}
113+
for err = HandleKubernetesComponents(wksp, api); errors.As(err, &retryErr); err = HandleKubernetesComponents(wksp, api) {
114+
i += 1
115+
assert.LessOrEqual(t, i, maxIters, "HandleKubernetesComponents did no complete within %d iterations", maxIters)
116+
}
117+
118+
if tt.Output.ErrRegexp != nil {
119+
if !assert.Error(t, err, "Expected error to be returned") {
120+
return
121+
}
122+
assert.Regexp(t, *tt.Output.ErrRegexp, err.Error())
123+
} else {
124+
if !assert.NoError(t, err, "Unexpected error returned") {
125+
return
126+
}
127+
for _, obj := range collectClusterObj(tt.Output.ExpectedObjects) {
128+
objType := reflect.TypeOf(obj).Elem()
129+
clusterObj := reflect.New(objType).Interface().(client.Object)
130+
err := testClient.Get(api.Ctx, types.NamespacedName{Name: obj.GetName(), Namespace: wksp.Namespace}, clusterObj)
131+
if !assert.NoError(t, err, "Expect object to be created on cluster: %s %s", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName()) {
132+
return
133+
}
134+
assert.True(t, cmp.Equal(obj, clusterObj, cmpopts.IgnoreTypes(metav1.ObjectMeta{}, metav1.TypeMeta{})),
135+
"Expect objects to match: %s", cmp.Diff(obj, clusterObj, cmpopts.IgnoreTypes(metav1.ObjectMeta{}, metav1.TypeMeta{})))
136+
assert.Equal(t, clusterObj.GetLabels()[constants.DevWorkspaceIDLabel], testID, "Object should get devworkspace ID label")
137+
assert.Equal(t, clusterObj.GetLabels()[constants.DevWorkspaceCreatorLabel], testCreatorID, "Object should get devworkspace ID label")
138+
assert.Contains(t, clusterObj.GetOwnerReferences(), metav1.OwnerReference{
139+
Kind: "DevWorkspace",
140+
APIVersion: "workspace.devfile.io/v1alpha2",
141+
Name: testDevWorkspaceName,
142+
UID: testDevWorkspaceUID,
143+
})
144+
}
145+
}
146+
})
147+
}
148+
}
149+
150+
func TestSecretAndConfigMapProvisioning(t *testing.T) {
151+
if err := InitializeDeserializer(testScheme); err != nil {
152+
t.Fatalf("Unexpected error: %s", err)
153+
}
154+
defer func() { decoder = nil }()
155+
156+
testClient := fake.NewClientBuilder().WithScheme(testScheme).Build()
157+
api := sync.ClusterAPI{
158+
Client: testClient,
159+
Scheme: testScheme,
160+
Logger: testlog.TestLogger{T: t},
161+
}
162+
wksp := &common.DevWorkspaceWithConfig{
163+
DevWorkspace: testDevWorkspace.DeepCopy(),
164+
}
165+
cmInline := `{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"test-configmap"},"data":{"test":"data"}}`
166+
secretInline := `{"apiVersion":"v1","kind":"Secret","metadata":{"name":"test-secret"},"data":{"test":"dGVzdAo="}}`
167+
wksp.Spec.Template.Components = append(wksp.Spec.Template.Components,
168+
dw.Component{
169+
Name: "test-configmap",
170+
ComponentUnion: dw.ComponentUnion{
171+
Kubernetes: &dw.KubernetesComponent{
172+
K8sLikeComponent: dw.K8sLikeComponent{
173+
DeployByDefault: pointer.BoolPtr(true),
174+
K8sLikeComponentLocation: dw.K8sLikeComponentLocation{
175+
Inlined: cmInline,
176+
},
177+
},
178+
},
179+
},
180+
},
181+
dw.Component{
182+
Name: "test-secret",
183+
ComponentUnion: dw.ComponentUnion{
184+
Kubernetes: &dw.KubernetesComponent{
185+
K8sLikeComponent: dw.K8sLikeComponent{
186+
DeployByDefault: pointer.BoolPtr(true),
187+
K8sLikeComponentLocation: dw.K8sLikeComponentLocation{
188+
Inlined: secretInline,
189+
},
190+
},
191+
},
192+
},
193+
},
194+
)
195+
// Repeat function as long as it returns RetryError
196+
i := 0
197+
maxIters := 30
198+
var err error
199+
retryErr := &RetryError{}
200+
for err = HandleKubernetesComponents(wksp, api); errors.As(err, &retryErr); err = HandleKubernetesComponents(wksp, api) {
201+
i += 1
202+
assert.LessOrEqual(t, i, maxIters, "HandleKubernetesComponents did no complete within %d iterations", maxIters)
203+
}
204+
if !assert.NoError(t, err) {
205+
return
206+
}
207+
208+
clusterConfigmap := &corev1.ConfigMap{}
209+
err = testClient.Get(api.Ctx, types.NamespacedName{Name: "test-configmap", Namespace: wksp.Namespace}, clusterConfigmap)
210+
if !assert.NoError(t, err, "Expect configmap 'test-configmap' to be created on cluster") {
211+
return
212+
}
213+
assert.Contains(t, clusterConfigmap.GetLabels(), constants.DevWorkspaceWatchConfigMapLabel)
214+
215+
clusterSecret := &corev1.Secret{}
216+
err = testClient.Get(api.Ctx, types.NamespacedName{Name: "test-secret", Namespace: wksp.Namespace}, clusterSecret)
217+
if !assert.NoError(t, err, "Expect secret 'test-secret' to be created on cluster") {
218+
return
219+
}
220+
assert.Contains(t, clusterSecret.GetLabels(), constants.DevWorkspaceWatchSecretLabel)
221+
}
222+
223+
func TestHasKubelikeComponent(t *testing.T) {
224+
noComponents := loadTestCaseOrPanic(t, "testdata/provision_tests/no-k8s-components-devworkspace.yaml")
225+
workspaceWithoutK8sComponents := &common.DevWorkspaceWithConfig{
226+
DevWorkspace: testDevWorkspace.DeepCopy(),
227+
}
228+
workspaceWithoutK8sComponents.Spec.Template.Components = append(workspaceWithoutK8sComponents.Spec.Template.Components, noComponents.Input.Components...)
229+
assert.False(t, HasKubelikeComponent(workspaceWithoutK8sComponents))
230+
231+
hasComponents := loadTestCaseOrPanic(t, "testdata/provision_tests/creates-k8s-objects.yaml")
232+
workspaceWithK8sComponents := &common.DevWorkspaceWithConfig{
233+
DevWorkspace: testDevWorkspace.DeepCopy(),
234+
}
235+
workspaceWithK8sComponents.Spec.Template.Components = append(workspaceWithK8sComponents.Spec.Template.Components, hasComponents.Input.Components...)
236+
assert.True(t, HasKubelikeComponent(workspaceWithK8sComponents))
237+
}
238+
239+
func loadAllTestCasesOrPanic(t *testing.T, fromDir string) []testCase {
240+
files, err := os.ReadDir(fromDir)
241+
if err != nil {
242+
t.Fatal(err)
243+
}
244+
var tests []testCase
245+
for _, file := range files {
246+
if file.IsDir() {
247+
tests = append(tests, loadAllTestCasesOrPanic(t, filepath.Join(fromDir, file.Name()))...)
248+
} else {
249+
tests = append(tests, loadTestCaseOrPanic(t, filepath.Join(fromDir, file.Name())))
250+
}
251+
}
252+
return tests
253+
}
254+
255+
func loadTestCaseOrPanic(t *testing.T, testPath string) testCase {
256+
bytes, err := os.ReadFile(testPath)
257+
if err != nil {
258+
t.Fatal(err)
259+
}
260+
var test testCase
261+
if err := yaml.Unmarshal(bytes, &test); err != nil {
262+
t.Fatal(err)
263+
}
264+
test.originalFilename = testPath
265+
return test
266+
}
267+
268+
func collectClusterObj(clusterObjs clusterObjects) []client.Object {
269+
var objs []client.Object
270+
for _, pod := range clusterObjs.Pods {
271+
pod := pod
272+
pod.Namespace = testNamespace
273+
objs = append(objs, &pod)
274+
}
275+
for _, svc := range clusterObjs.Services {
276+
svc := svc
277+
svc.Namespace = testNamespace
278+
objs = append(objs, &svc)
279+
}
280+
return objs
281+
}

0 commit comments

Comments
 (0)