Skip to content

Commit d69efe9

Browse files
committed
Add DevWorkspace controller tests for automount functionality
Add test cases covering automatic mounting of image pull secrets, configmaps/secrets, git credentials. Signed-off-by: Angel Misevski <[email protected]>
1 parent 8fef40d commit d69efe9

File tree

2 files changed

+334
-0
lines changed

2 files changed

+334
-0
lines changed

controllers/workspace/devworkspace_controller_test.go

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,4 +286,239 @@ var _ = Describe("DevWorkspace Controller", func() {
286286
})
287287

288288
})
289+
290+
Context("Automatic provisioning", func() {
291+
const testURL = "test-url"
292+
293+
BeforeEach(func() {
294+
workspacecontroller.SetupHttpClientsForTesting(&http.Client{
295+
Transport: &testutil.TestRoundTripper{
296+
Data: map[string]testutil.TestResponse{
297+
fmt.Sprintf("%s/healthz", testURL): {
298+
StatusCode: http.StatusOK,
299+
},
300+
},
301+
},
302+
})
303+
createDevWorkspace("test-devworkspace.yaml")
304+
})
305+
306+
AfterEach(func() {
307+
deleteDevWorkspace(devWorkspaceName)
308+
workspacecontroller.SetupHttpClientsForTesting(getBasicTestHttpClient())
309+
})
310+
311+
It("Mounts image pull secrets to the DevWorkspace Deployment", func() {
312+
devworkspace := getExistingDevWorkspace()
313+
workspaceID := devworkspace.Status.DevWorkspaceId
314+
315+
By("Creating secrets for docker configs")
316+
dockerCfgSecretName := "test-dockercfg"
317+
dockerCfg := generateSecret(dockerCfgSecretName, corev1.SecretTypeDockercfg)
318+
dockerCfg.Labels[constants.DevWorkspacePullSecretLabel] = "true"
319+
dockerCfg.Data[".dockercfg"] = []byte("{}")
320+
createObject(dockerCfg)
321+
defer deleteObject(dockerCfg)
322+
323+
dockerCfgSecretJsonName := "test-dockercfg-json"
324+
dockerCfgJson := generateSecret(dockerCfgSecretJsonName, corev1.SecretTypeDockerConfigJson)
325+
dockerCfgJson.Labels[constants.DevWorkspacePullSecretLabel] = "true"
326+
dockerCfgJson.Data[".dockerconfigjson"] = []byte("{}")
327+
createObject(dockerCfgJson)
328+
defer deleteObject(dockerCfgJson)
329+
330+
By("Manually making Routing ready to continue")
331+
markRoutingReady(testURL, common.DevWorkspaceRoutingName(workspaceID))
332+
333+
deploy := &appsv1.Deployment{}
334+
deployNN := types.NamespacedName{
335+
Name: common.DeploymentName(workspaceID),
336+
Namespace: testNamespace,
337+
}
338+
Eventually(func() error {
339+
return k8sClient.Get(ctx, deployNN, deploy)
340+
}, timeout, interval).Should(Succeed(), "Getting workspace deployment from cluster")
341+
342+
Expect(deploy.Spec.Template.Spec.ImagePullSecrets).Should(ContainElement(corev1.LocalObjectReference{Name: dockerCfgSecretName}))
343+
Expect(deploy.Spec.Template.Spec.ImagePullSecrets).Should(ContainElement(corev1.LocalObjectReference{Name: dockerCfgSecretJsonName}))
344+
})
345+
346+
It("Manages git credentials for DevWorkspace", func() {
347+
devworkspace := getExistingDevWorkspace()
348+
workspaceID := devworkspace.Status.DevWorkspaceId
349+
350+
By("Creating a secret for git credentials")
351+
gitCredentialsSecretName := "test-git-credentials"
352+
gitCredentials := generateSecret(gitCredentialsSecretName, corev1.SecretTypeOpaque)
353+
gitCredentials.Labels[constants.DevWorkspaceGitCredentialLabel] = "true"
354+
gitCredentials.Annotations[constants.DevWorkspaceMountPathAnnotation] = "/test/path"
355+
gitCredentials.Data["credentials"] = []byte("https://username:[email protected]")
356+
357+
createObject(gitCredentials)
358+
defer deleteObject(gitCredentials)
359+
360+
By("Manually making Routing ready to continue")
361+
markRoutingReady(testURL, common.DevWorkspaceRoutingName(workspaceID))
362+
363+
deploy := &appsv1.Deployment{}
364+
deployNN := types.NamespacedName{
365+
Name: common.DeploymentName(workspaceID),
366+
Namespace: testNamespace,
367+
}
368+
Eventually(func() error {
369+
return k8sClient.Get(ctx, deployNN, deploy)
370+
}, timeout, interval).Should(Succeed(), "Getting workspace deployment from cluster")
371+
372+
modeReadOnly := int32(0640)
373+
gitconfigVolumeName := common.AutoMountConfigMapVolumeName("devworkspace-gitconfig")
374+
gitconfigVolume := corev1.Volume{
375+
Name: gitconfigVolumeName,
376+
VolumeSource: corev1.VolumeSource{
377+
ConfigMap: &corev1.ConfigMapVolumeSource{
378+
LocalObjectReference: corev1.LocalObjectReference{Name: "devworkspace-gitconfig"},
379+
DefaultMode: &modeReadOnly,
380+
},
381+
},
382+
}
383+
gitConfigVolumeMount := corev1.VolumeMount{
384+
Name: gitconfigVolumeName,
385+
ReadOnly: false,
386+
MountPath: "/etc/gitconfig",
387+
SubPath: "gitconfig",
388+
}
389+
gitCredentialsVolumeName := common.AutoMountSecretVolumeName("devworkspace-merged-git-credentials")
390+
gitCredentialsVolume := corev1.Volume{
391+
Name: gitCredentialsVolumeName,
392+
VolumeSource: corev1.VolumeSource{
393+
Secret: &corev1.SecretVolumeSource{
394+
SecretName: "devworkspace-merged-git-credentials",
395+
DefaultMode: &modeReadOnly,
396+
},
397+
},
398+
}
399+
gitCredentialsVolumeMount := corev1.VolumeMount{
400+
Name: gitCredentialsVolumeName,
401+
ReadOnly: true,
402+
MountPath: "/test/path/credentials",
403+
SubPath: "credentials",
404+
}
405+
406+
volumes := deploy.Spec.Template.Spec.Volumes
407+
Expect(volumes).Should(ContainElements(gitconfigVolume, gitCredentialsVolume), "Git credentials should be mounted as volumes in Deployment")
408+
for _, container := range deploy.Spec.Template.Spec.Containers {
409+
Expect(container.VolumeMounts).Should(ContainElements(gitConfigVolumeMount, gitCredentialsVolumeMount))
410+
}
411+
})
412+
413+
It("Automounts secrets and configmaps volumes", func() {
414+
devworkspace := getExistingDevWorkspace()
415+
workspaceID := devworkspace.Status.DevWorkspaceId
416+
417+
By("Creating a automount secrets and configmaps")
418+
fileCM := generateConfigMap("file-cm")
419+
fileCM.Labels[constants.DevWorkspaceMountLabel] = "true"
420+
fileCM.Annotations[constants.DevWorkspaceMountPathAnnotation] = "/file/cm"
421+
createObject(fileCM)
422+
defer deleteObject(fileCM)
423+
424+
subpathCM := generateConfigMap("subpath-cm")
425+
subpathCM.Labels[constants.DevWorkspaceMountLabel] = "true"
426+
subpathCM.Annotations[constants.DevWorkspaceMountPathAnnotation] = "/subpath/cm"
427+
subpathCM.Annotations[constants.DevWorkspaceMountAsAnnotation] = "subpath"
428+
subpathCM.Data["testdata1"] = "testValue"
429+
subpathCM.Data["testdata2"] = "testValue"
430+
createObject(subpathCM)
431+
defer deleteObject(subpathCM)
432+
433+
fileSecret := generateSecret("file-secret", corev1.SecretTypeOpaque)
434+
fileSecret.Labels[constants.DevWorkspaceMountLabel] = "true"
435+
fileSecret.Annotations[constants.DevWorkspaceMountPathAnnotation] = "/file/secret"
436+
createObject(fileSecret)
437+
defer deleteObject(fileSecret)
438+
439+
subpathSecret := generateSecret("subpath-secret", corev1.SecretTypeOpaque)
440+
subpathSecret.Labels[constants.DevWorkspaceMountLabel] = "true"
441+
subpathSecret.Annotations[constants.DevWorkspaceMountPathAnnotation] = "/subpath/secret"
442+
subpathSecret.Annotations[constants.DevWorkspaceMountAsAnnotation] = "subpath"
443+
subpathSecret.Data["testsecret"] = []byte("testValue")
444+
createObject(subpathSecret)
445+
defer deleteObject(subpathSecret)
446+
447+
By("Manually making Routing ready to continue")
448+
markRoutingReady(testURL, common.DevWorkspaceRoutingName(workspaceID))
449+
450+
deploy := &appsv1.Deployment{}
451+
deployNN := types.NamespacedName{
452+
Name: common.DeploymentName(workspaceID),
453+
Namespace: testNamespace,
454+
}
455+
Eventually(func() error {
456+
return k8sClient.Get(ctx, deployNN, deploy)
457+
}, timeout, interval).Should(Succeed(), "Getting workspace deployment from cluster")
458+
459+
expectedAutomountVolumes := []corev1.Volume{
460+
volumeFromConfigMap(fileCM),
461+
volumeFromConfigMap(subpathCM),
462+
volumeFromSecret(fileSecret),
463+
volumeFromSecret(subpathSecret),
464+
}
465+
Expect(deploy.Spec.Template.Spec.Volumes).Should(ContainElements(expectedAutomountVolumes), "Automount volumes should be added to deployment")
466+
expectedAutomountVolumeMounts := []corev1.VolumeMount{
467+
volumeMountFromConfigMap(fileCM, "/file/cm", ""),
468+
volumeMountFromConfigMap(subpathCM, "/subpath/cm", "testdata1"),
469+
volumeMountFromConfigMap(subpathCM, "/subpath/cm", "testdata2"),
470+
volumeMountFromSecret(fileSecret, "/file/secret", ""),
471+
volumeMountFromSecret(subpathSecret, "/subpath/secret", "testsecret"),
472+
}
473+
for _, container := range deploy.Spec.Template.Spec.Containers {
474+
Expect(container.VolumeMounts).Should(ContainElements(expectedAutomountVolumeMounts), "Automount volumeMounts should be added to all containers")
475+
}
476+
})
477+
478+
It("Automounts secrets and configmaps env vars", func() {
479+
devworkspace := getExistingDevWorkspace()
480+
workspaceID := devworkspace.Status.DevWorkspaceId
481+
482+
By("Creating a automount secrets and configmaps")
483+
cm := generateConfigMap("env-cm")
484+
cm.Labels[constants.DevWorkspaceMountLabel] = "true"
485+
cm.Annotations[constants.DevWorkspaceMountAsAnnotation] = "env"
486+
createObject(cm)
487+
defer deleteObject(cm)
488+
489+
secret := generateSecret("env-secret", corev1.SecretTypeOpaque)
490+
secret.Labels[constants.DevWorkspaceMountLabel] = "true"
491+
secret.Annotations[constants.DevWorkspaceMountAsAnnotation] = "env"
492+
createObject(secret)
493+
defer deleteObject(secret)
494+
495+
By("Manually making Routing ready to continue")
496+
markRoutingReady(testURL, common.DevWorkspaceRoutingName(workspaceID))
497+
deploy := &appsv1.Deployment{}
498+
deployNN := types.NamespacedName{
499+
Name: common.DeploymentName(workspaceID),
500+
Namespace: testNamespace,
501+
}
502+
Eventually(func() error {
503+
return k8sClient.Get(ctx, deployNN, deploy)
504+
}, timeout, interval).Should(Succeed(), "Getting workspace deployment from cluster")
505+
506+
expectedEnvFromSources := []corev1.EnvFromSource{
507+
{
508+
ConfigMapRef: &corev1.ConfigMapEnvSource{
509+
LocalObjectReference: corev1.LocalObjectReference{Name: cm.Name},
510+
},
511+
},
512+
{
513+
SecretRef: &corev1.SecretEnvSource{
514+
LocalObjectReference: corev1.LocalObjectReference{Name: secret.Name},
515+
},
516+
},
517+
}
518+
for _, container := range deploy.Spec.Template.Spec.Containers {
519+
Expect(container.EnvFrom).Should(ContainElements(expectedEnvFromSources), "Automounted env sources should be added to containers")
520+
}
521+
})
522+
})
523+
289524
})

controllers/workspace/util_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,17 @@
1414
package controllers_test
1515

1616
import (
17+
"path"
1718
"time"
1819

1920
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
2021
controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
22+
"github.com/devfile/devworkspace-operator/pkg/common"
23+
"github.com/devfile/devworkspace-operator/pkg/constants"
2124
. "github.com/onsi/ginkgo/v2"
2225
. "github.com/onsi/gomega"
26+
corev1 "k8s.io/api/core/v1"
27+
crclient "sigs.k8s.io/controller-runtime/pkg/client"
2328

2429
appsv1 "k8s.io/api/apps/v1"
2530
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
@@ -90,6 +95,21 @@ func deleteDevWorkspace(name string) {
9095
}).Should(BeTrue(), "DevWorkspace not deleted after timeout")
9196
}
9297

98+
func createObject(obj crclient.Object) {
99+
Expect(k8sClient.Create(ctx, obj)).Should(Succeed())
100+
Eventually(func() error {
101+
return k8sClient.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, obj)
102+
}, 10*time.Second, 250*time.Millisecond).Should(Succeed(), "Creating %s with name %s", obj.GetObjectKind(), obj.GetName())
103+
}
104+
105+
func deleteObject(obj crclient.Object) {
106+
Expect(k8sClient.Delete(ctx, obj)).Should(Succeed())
107+
Eventually(func() bool {
108+
err := k8sClient.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, obj)
109+
return k8sErrors.IsNotFound(err)
110+
}, 10*time.Second, 250*time.Millisecond).Should(BeTrue(), "Deleting %s with name %s", obj.GetObjectKind(), obj.GetName())
111+
}
112+
93113
func markRoutingReady(mainUrl, routingName string) {
94114
namespacedName := types.NamespacedName{
95115
Name: routingName,
@@ -148,3 +168,82 @@ func devworkspaceOwnerRef(wksp *dw.DevWorkspace) metav1.OwnerReference {
148168
BlockOwnerDeletion: &boolTrue,
149169
}
150170
}
171+
172+
func generateSecret(name string, secretType corev1.SecretType) *corev1.Secret {
173+
return &corev1.Secret{
174+
ObjectMeta: metav1.ObjectMeta{
175+
Name: name,
176+
Namespace: testNamespace,
177+
Labels: map[string]string{
178+
constants.DevWorkspaceWatchSecretLabel: "true",
179+
},
180+
Annotations: map[string]string{},
181+
},
182+
Type: secretType,
183+
Data: map[string][]byte{},
184+
}
185+
}
186+
187+
func generateConfigMap(name string) *corev1.ConfigMap {
188+
return &corev1.ConfigMap{
189+
ObjectMeta: metav1.ObjectMeta{
190+
Name: name,
191+
Namespace: testNamespace,
192+
Labels: map[string]string{
193+
constants.DevWorkspaceWatchConfigMapLabel: "true",
194+
},
195+
Annotations: map[string]string{},
196+
},
197+
Data: map[string]string{},
198+
}
199+
}
200+
201+
func volumeFromSecret(secret *corev1.Secret) corev1.Volume {
202+
modeReadOnly := int32(0640)
203+
return corev1.Volume{
204+
Name: common.AutoMountSecretVolumeName(secret.Name),
205+
VolumeSource: corev1.VolumeSource{
206+
Secret: &corev1.SecretVolumeSource{
207+
SecretName: secret.Name,
208+
DefaultMode: &modeReadOnly,
209+
},
210+
},
211+
}
212+
}
213+
214+
func volumeFromConfigMap(cm *corev1.ConfigMap) corev1.Volume {
215+
modeReadOnly := int32(0640)
216+
return corev1.Volume{
217+
Name: common.AutoMountConfigMapVolumeName(cm.Name),
218+
VolumeSource: corev1.VolumeSource{
219+
ConfigMap: &corev1.ConfigMapVolumeSource{
220+
LocalObjectReference: corev1.LocalObjectReference{Name: cm.Name},
221+
DefaultMode: &modeReadOnly,
222+
},
223+
},
224+
}
225+
}
226+
227+
func volumeMountFromSecret(secret *corev1.Secret, mountPath, subPath string) corev1.VolumeMount {
228+
if subPath != "" {
229+
mountPath = path.Join(mountPath, subPath)
230+
}
231+
return corev1.VolumeMount{
232+
Name: common.AutoMountSecretVolumeName(secret.Name),
233+
ReadOnly: true,
234+
MountPath: mountPath,
235+
SubPath: subPath,
236+
}
237+
}
238+
239+
func volumeMountFromConfigMap(cm *corev1.ConfigMap, mountPath, subPath string) corev1.VolumeMount {
240+
if subPath != "" {
241+
mountPath = path.Join(mountPath, subPath)
242+
}
243+
return corev1.VolumeMount{
244+
Name: common.AutoMountConfigMapVolumeName(cm.Name),
245+
ReadOnly: true,
246+
MountPath: mountPath,
247+
SubPath: subPath,
248+
}
249+
}

0 commit comments

Comments
 (0)