Skip to content

Commit 06a1e28

Browse files
committed
test: unit and e2e tests for init containers
Signed-off-by: Oleksii Kurinnyi <[email protected]>
1 parent c6b803d commit 06a1e28

File tree

9 files changed

+876
-1
lines changed

9 files changed

+876
-1
lines changed
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
//
2+
// Copyright (c) 2019-2025 Red Hat, Inc.
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
package controllers
17+
18+
import (
19+
"testing"
20+
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/common"
24+
"github.com/devfile/devworkspace-operator/pkg/constants"
25+
"github.com/stretchr/testify/assert"
26+
corev1 "k8s.io/api/core/v1"
27+
"k8s.io/apimachinery/pkg/api/resource"
28+
)
29+
30+
func TestDefaultAndValidateHomeInitContainer(t *testing.T) {
31+
workspace := &common.DevWorkspaceWithConfig{
32+
DevWorkspace: &dw.DevWorkspace{
33+
Spec: dw.DevWorkspaceSpec{
34+
Template: dw.DevWorkspaceTemplateSpec{
35+
DevWorkspaceTemplateSpecContent: dw.DevWorkspaceTemplateSpecContent{
36+
Components: []dw.Component{
37+
{
38+
Name: "main-container",
39+
ComponentUnion: dw.ComponentUnion{
40+
Container: &dw.ContainerComponent{
41+
Container: dw.Container{
42+
Image: "test-image:latest",
43+
},
44+
},
45+
},
46+
},
47+
},
48+
},
49+
},
50+
},
51+
},
52+
Config: &v1alpha1.OperatorConfiguration{
53+
Workspace: &v1alpha1.WorkspaceConfig{},
54+
},
55+
}
56+
57+
tests := []struct {
58+
name string
59+
container corev1.Container
60+
expectError bool
61+
errorMsg string
62+
validate func(t *testing.T, result corev1.Container)
63+
}{
64+
{
65+
name: "Defaults image when empty",
66+
container: corev1.Container{
67+
Name: constants.HomeInitComponentName,
68+
Args: []string{"echo 'test'"},
69+
},
70+
expectError: false,
71+
validate: func(t *testing.T, result corev1.Container) {
72+
assert.Equal(t, "test-image:latest", result.Image)
73+
},
74+
},
75+
{
76+
name: "Defaults command when empty",
77+
container: corev1.Container{
78+
Name: constants.HomeInitComponentName,
79+
Image: "custom-image:latest",
80+
Args: []string{"echo 'test'"},
81+
},
82+
expectError: false,
83+
validate: func(t *testing.T, result corev1.Container) {
84+
assert.Equal(t, []string{"/bin/sh", "-c"}, result.Command)
85+
},
86+
},
87+
{
88+
name: "Accepts valid command",
89+
container: corev1.Container{
90+
Name: constants.HomeInitComponentName,
91+
Image: "custom-image:latest",
92+
Command: []string{"/bin/sh", "-c"},
93+
Args: []string{"echo 'test'"},
94+
},
95+
expectError: false,
96+
},
97+
{
98+
name: "Rejects invalid command",
99+
container: corev1.Container{
100+
Name: constants.HomeInitComponentName,
101+
Image: "custom-image:latest",
102+
Command: []string{"/bin/bash"},
103+
Args: []string{"echo 'test'"},
104+
},
105+
expectError: true,
106+
errorMsg: "command must be exactly [/bin/sh, -c]",
107+
},
108+
{
109+
name: "Rejects empty args",
110+
container: corev1.Container{
111+
Name: constants.HomeInitComponentName,
112+
Image: "custom-image:latest",
113+
Args: []string{},
114+
},
115+
expectError: true,
116+
errorMsg: "args must contain exactly one script string",
117+
},
118+
{
119+
name: "Rejects multiple args",
120+
container: corev1.Container{
121+
Name: constants.HomeInitComponentName,
122+
Image: "custom-image:latest",
123+
Args: []string{"echo 'test'", "echo 'test2'"},
124+
},
125+
expectError: true,
126+
errorMsg: "args must contain exactly one script string",
127+
},
128+
{
129+
name: "Rejects user-provided volumeMounts",
130+
container: corev1.Container{
131+
Name: constants.HomeInitComponentName,
132+
Image: "custom-image:latest",
133+
Args: []string{"echo 'test'"},
134+
VolumeMounts: []corev1.VolumeMount{
135+
{
136+
Name: "custom-volume",
137+
MountPath: "/mnt/custom",
138+
},
139+
},
140+
},
141+
expectError: true,
142+
errorMsg: "volumeMounts are not allowed for init-persistent-home",
143+
},
144+
{
145+
name: "Injects persistent-home volumeMount",
146+
container: corev1.Container{
147+
Name: constants.HomeInitComponentName,
148+
Image: "custom-image:latest",
149+
Args: []string{"echo 'test'"},
150+
},
151+
expectError: false,
152+
validate: func(t *testing.T, result corev1.Container) {
153+
assert.Len(t, result.VolumeMounts, 1)
154+
assert.Equal(t, constants.HomeVolumeName, result.VolumeMounts[0].Name)
155+
assert.Equal(t, constants.HomeUserDirectory, result.VolumeMounts[0].MountPath)
156+
},
157+
},
158+
{
159+
name: "Allows env variables",
160+
container: corev1.Container{
161+
Name: constants.HomeInitComponentName,
162+
Image: "custom-image:latest",
163+
Args: []string{"echo 'test'"},
164+
Env: []corev1.EnvVar{
165+
{Name: "TEST_VAR", Value: "test-value"},
166+
},
167+
},
168+
expectError: false,
169+
validate: func(t *testing.T, result corev1.Container) {
170+
assert.Len(t, result.Env, 1)
171+
assert.Equal(t, "TEST_VAR", result.Env[0].Name)
172+
},
173+
},
174+
{
175+
name: "Rejects ports",
176+
container: corev1.Container{
177+
Name: constants.HomeInitComponentName,
178+
Image: "custom-image:latest",
179+
Args: []string{"echo 'test'"},
180+
Ports: []corev1.ContainerPort{
181+
{ContainerPort: 8080},
182+
},
183+
},
184+
expectError: true,
185+
errorMsg: "ports are not allowed",
186+
},
187+
{
188+
name: "Rejects livenessProbe",
189+
container: corev1.Container{
190+
Name: constants.HomeInitComponentName,
191+
Image: "custom-image:latest",
192+
Args: []string{"echo 'test'"},
193+
LivenessProbe: &corev1.Probe{
194+
ProbeHandler: corev1.ProbeHandler{
195+
HTTPGet: &corev1.HTTPGetAction{Path: "/health"},
196+
},
197+
},
198+
},
199+
expectError: true,
200+
errorMsg: "probes are not allowed",
201+
},
202+
{
203+
name: "Rejects securityContext",
204+
container: corev1.Container{
205+
Name: constants.HomeInitComponentName,
206+
Image: "custom-image:latest",
207+
Args: []string{"echo 'test'"},
208+
SecurityContext: &corev1.SecurityContext{
209+
RunAsUser: new(int64),
210+
},
211+
},
212+
expectError: true,
213+
errorMsg: "securityContext is not allowed",
214+
},
215+
{
216+
name: "Rejects resources",
217+
container: corev1.Container{
218+
Name: constants.HomeInitComponentName,
219+
Image: "custom-image:latest",
220+
Args: []string{"echo 'test'"},
221+
Resources: corev1.ResourceRequirements{
222+
Limits: corev1.ResourceList{
223+
corev1.ResourceMemory: resource.MustParse("128Mi"),
224+
},
225+
},
226+
},
227+
expectError: true,
228+
errorMsg: "resource limits/requests are not allowed",
229+
},
230+
{
231+
name: "Rejects workingDir",
232+
container: corev1.Container{
233+
Name: constants.HomeInitComponentName,
234+
Image: "custom-image:latest",
235+
Args: []string{"echo 'test'"},
236+
WorkingDir: "/tmp",
237+
},
238+
expectError: true,
239+
errorMsg: "volumeDevices and workingDir are not allowed",
240+
},
241+
{
242+
name: "Rejects image with whitespace",
243+
container: corev1.Container{
244+
Name: constants.HomeInitComponentName,
245+
Image: "nginx\nmalicious",
246+
Args: []string{"echo 'test'"},
247+
},
248+
expectError: true,
249+
errorMsg: "invalid image reference",
250+
},
251+
}
252+
253+
for _, tt := range tests {
254+
t.Run(tt.name, func(t *testing.T) {
255+
result, err := defaultAndValidateHomeInitContainer(tt.container, workspace)
256+
257+
if tt.expectError {
258+
assert.Error(t, err)
259+
if tt.errorMsg != "" {
260+
assert.Contains(t, err.Error(), tt.errorMsg)
261+
}
262+
} else {
263+
assert.NoError(t, err)
264+
if tt.validate != nil {
265+
tt.validate(t, result)
266+
}
267+
}
268+
})
269+
}
270+
}
271+
272+
func TestDefaultAndValidateHomeInitContainer_NoWorkspaceImage(t *testing.T) {
273+
workspaceNoImage := &common.DevWorkspaceWithConfig{
274+
DevWorkspace: &dw.DevWorkspace{
275+
Spec: dw.DevWorkspaceSpec{
276+
Template: dw.DevWorkspaceTemplateSpec{
277+
DevWorkspaceTemplateSpecContent: dw.DevWorkspaceTemplateSpecContent{
278+
Components: []dw.Component{
279+
{
280+
Name: "volume-component",
281+
ComponentUnion: dw.ComponentUnion{
282+
Volume: &dw.VolumeComponent{},
283+
},
284+
},
285+
},
286+
},
287+
},
288+
},
289+
},
290+
Config: &v1alpha1.OperatorConfiguration{
291+
Workspace: &v1alpha1.WorkspaceConfig{},
292+
},
293+
}
294+
295+
container := corev1.Container{
296+
Name: constants.HomeInitComponentName,
297+
Args: []string{"echo 'test'"},
298+
}
299+
300+
_, err := defaultAndValidateHomeInitContainer(container, workspaceNoImage)
301+
assert.Error(t, err)
302+
assert.Contains(t, err.Error(), "unable to infer workspace image")
303+
}

pkg/config/sync_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,10 @@ func TestMergesAllFieldsFromClusterConfig(t *testing.T) {
114114
for i := 0; i < 100; i++ {
115115
fuzzedConfig := &v1alpha1.OperatorConfiguration{}
116116
f.Fuzz(fuzzedConfig)
117-
// Skip checking these two fields as they're interface fields and hard to fuzz.
117+
// Skip checking these fields as they're interface fields and hard to fuzz.
118118
fuzzedConfig.Workspace.DefaultStorageSize = defaultConfig.Workspace.DefaultStorageSize.DeepCopy()
119119
fuzzedConfig.Workspace.PodSecurityContext = defaultConfig.Workspace.PodSecurityContext.DeepCopy()
120+
fuzzedConfig.Workspace.InitContainers = defaultConfig.Workspace.InitContainers
120121
clusterConfig := buildConfig(fuzzedConfig)
121122
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(clusterConfig).Build()
122123
err := SetupControllerConfig(client)

0 commit comments

Comments
 (0)