563 lines
22 KiB
Go
563 lines
22 KiB
Go
/*
|
|
Copyright 2024.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package webhook
|
|
|
|
import (
|
|
"slices"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
gomegaTypes "github.com/onsi/gomega/types"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
|
|
)
|
|
|
|
var _ = Describe("WorkspaceKind Webhook", func() {
|
|
|
|
const (
|
|
namespaceName = "default"
|
|
)
|
|
|
|
Context("When creating a WorkspaceKind", Ordered, func() {
|
|
|
|
testCases := []struct {
|
|
// the "Should()" description of the test
|
|
description string
|
|
|
|
// the WorkspaceKind to attempt to create
|
|
workspaceKind *kubefloworgv1beta1.WorkspaceKind
|
|
|
|
// if the test should succeed
|
|
shouldSucceed bool
|
|
}{
|
|
{
|
|
description: "should accept creation of a valid WorkspaceKind",
|
|
workspaceKind: NewExampleWorkspaceKind("wsk-webhook-create--valid"),
|
|
shouldSucceed: true,
|
|
},
|
|
{
|
|
description: "should reject creation with cycle in imageConfig redirects",
|
|
workspaceKind: NewExampleWorkspaceKindWithImageConfigCycle("wsk-webhook-create--image-config-cycle"),
|
|
shouldSucceed: false,
|
|
},
|
|
{
|
|
description: "should reject creation with cycle in podConfig redirects",
|
|
workspaceKind: NewExampleWorkspaceKindWithPodConfigCycle("wsk-webhook-create--pod-config-cycle"),
|
|
shouldSucceed: false,
|
|
},
|
|
{
|
|
description: "should reject creation with invalid redirect target in imageConfig options",
|
|
workspaceKind: NewExampleWorkspaceKindWithInvalidImageConfigRedirect("wsk-webhook-create--image-config-invalid-redirect"),
|
|
shouldSucceed: false,
|
|
},
|
|
{
|
|
description: "should reject creation with invalid redirect target in podConfig options",
|
|
workspaceKind: NewExampleWorkspaceKindWithInvalidPodConfigRedirect("wsk-webhook-create--pod-config-invalid-redirect"),
|
|
shouldSucceed: false,
|
|
},
|
|
{
|
|
description: "should reject creation with invalid default imageConfig",
|
|
workspaceKind: NewExampleWorkspaceKindWithInvalidDefaultImageConfig("wsk-webhook-create--image-config-invalid-default"),
|
|
shouldSucceed: false,
|
|
},
|
|
{
|
|
description: "should reject creation with invalid default podConfig",
|
|
workspaceKind: NewExampleWorkspaceKindWithInvalidDefaultPodConfig("wsk-webhook-create--pod-config-invalid-default"),
|
|
shouldSucceed: false,
|
|
},
|
|
{
|
|
description: "should reject creation with duplicate ports in imageConfig",
|
|
workspaceKind: NewExampleWorkspaceKindWithDuplicatePorts("wsk-webhook-create--image-config-duplicate-ports"),
|
|
shouldSucceed: false,
|
|
},
|
|
{
|
|
description: "should reject creation if extraEnv[].value is not a valid Go template",
|
|
workspaceKind: NewExampleWorkspaceKindWithInvalidExtraEnvValue("wsk-webhook-create--extra-invalid-env-value"),
|
|
shouldSucceed: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
It(tc.description, func() {
|
|
if tc.shouldSucceed {
|
|
By("creating the WorkspaceKind")
|
|
Expect(k8sClient.Create(ctx, tc.workspaceKind)).To(Succeed())
|
|
|
|
By("deleting the WorkspaceKind")
|
|
Expect(k8sClient.Delete(ctx, tc.workspaceKind)).To(Succeed())
|
|
} else {
|
|
By("creating the WorkspaceKind")
|
|
Expect(k8sClient.Create(ctx, tc.workspaceKind)).NotTo(Succeed())
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
Context("When updating a WorkspaceKind", Ordered, func() {
|
|
const (
|
|
workspaceName = "wsk-webhook-update-test"
|
|
workspaceKindName = "wsk-webhook-update-test"
|
|
)
|
|
|
|
AfterEach(func() {
|
|
By("deleting the Workspace, if it exists")
|
|
workspace := &kubefloworgv1beta1.Workspace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: workspaceName,
|
|
Namespace: namespaceName,
|
|
},
|
|
}
|
|
_ = k8sClient.Delete(ctx, workspace)
|
|
|
|
By("deleting the WorkspaceKind, if it exists")
|
|
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: workspaceKindName,
|
|
},
|
|
}
|
|
_ = k8sClient.Delete(ctx, workspaceKind)
|
|
})
|
|
|
|
testCases := []struct {
|
|
// the "Should()" description of the test
|
|
description string
|
|
|
|
// if the test should succeed
|
|
shouldSucceed bool
|
|
|
|
// the initial state of the WorkspaceKind (required)
|
|
workspaceKind *kubefloworgv1beta1.WorkspaceKind
|
|
|
|
// the initial state of the Workspace, if any
|
|
workspace *kubefloworgv1beta1.Workspace
|
|
|
|
// modifyKindFn modifies the WorkspaceKind in some way.
|
|
// returns a string matcher for the error message (only used if `shouldSucceed` is false)
|
|
modifyKindFn func(*kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher
|
|
}{
|
|
{
|
|
description: "should accept re-ordering in-use imageConfig values",
|
|
shouldSucceed: true,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
workspace: NewExampleWorkspace(workspaceName, namespaceName, workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
// reverse the imageConfig values list
|
|
slices.Reverse(wsk.Spec.PodTemplate.Options.ImageConfig.Values)
|
|
return ContainSubstring("")
|
|
},
|
|
},
|
|
{
|
|
description: "should accept re-ordering in-use podConfig values",
|
|
shouldSucceed: true,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
workspace: NewExampleWorkspace(workspaceName, namespaceName, workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
// reverse the podConfig values list
|
|
slices.Reverse(wsk.Spec.PodTemplate.Options.PodConfig.Values)
|
|
return ContainSubstring("")
|
|
},
|
|
},
|
|
{
|
|
description: "should reject updates to in-use imageConfig spec",
|
|
shouldSucceed: false,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
workspace: NewExampleWorkspace(workspaceName, namespaceName, workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
inUseId := wsk.Spec.PodTemplate.Options.ImageConfig.Values[0].Id
|
|
wsk.Spec.PodTemplate.Options.ImageConfig.Values[0].Spec.Image = "new-image:latest"
|
|
return ContainSubstring("imageConfig value %q is in use and cannot be changed", inUseId)
|
|
},
|
|
},
|
|
{
|
|
description: "should reject updates to in-use podConfig spec",
|
|
shouldSucceed: false,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
workspace: NewExampleWorkspace(workspaceName, namespaceName, workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
inUseId := wsk.Spec.PodTemplate.Options.PodConfig.Values[0].Id
|
|
wsk.Spec.PodTemplate.Options.PodConfig.Values[0].Spec.Resources = &corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{
|
|
corev1.ResourceCPU: resource.MustParse("1.5"),
|
|
},
|
|
}
|
|
return ContainSubstring("podConfig value %q is in use and cannot be changed", inUseId)
|
|
},
|
|
},
|
|
{
|
|
description: "should accept updates to unused imageConfig spec",
|
|
shouldSucceed: true,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
workspace: NewExampleWorkspace(workspaceName, namespaceName, workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
wsk.Spec.PodTemplate.Options.ImageConfig.Values[1].Spec.Image = "new-image:latest"
|
|
return ContainSubstring("")
|
|
},
|
|
},
|
|
{
|
|
description: "should accept updates to unused podConfig spec",
|
|
shouldSucceed: true,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
workspace: NewExampleWorkspace(workspaceName, namespaceName, workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
wsk.Spec.PodTemplate.Options.PodConfig.Values[1].Spec.Resources = &corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{
|
|
corev1.ResourceCPU: resource.MustParse("1.5"),
|
|
},
|
|
}
|
|
return ContainSubstring("")
|
|
},
|
|
},
|
|
{
|
|
description: "should reject removing in-use imageConfig values",
|
|
shouldSucceed: false,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
workspace: NewExampleWorkspace(workspaceName, namespaceName, workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
toBeRemoved := "jupyterlab_scipy_180"
|
|
newValues := make([]kubefloworgv1beta1.ImageConfigValue, 0)
|
|
for _, value := range wsk.Spec.PodTemplate.Options.ImageConfig.Values {
|
|
if value.Id != toBeRemoved {
|
|
newValues = append(newValues, value)
|
|
}
|
|
}
|
|
wsk.Spec.PodTemplate.Options.ImageConfig.Values = newValues
|
|
return ContainSubstring("imageConfig value %q is in use and cannot be removed", toBeRemoved)
|
|
},
|
|
},
|
|
{
|
|
description: "should reject removing in-use podConfig values",
|
|
shouldSucceed: false,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
workspace: NewExampleWorkspace(workspaceName, namespaceName, workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
toBeRemoved := "tiny_cpu"
|
|
newValues := make([]kubefloworgv1beta1.PodConfigValue, 0)
|
|
for _, value := range wsk.Spec.PodTemplate.Options.PodConfig.Values {
|
|
if value.Id != toBeRemoved {
|
|
newValues = append(newValues, value)
|
|
}
|
|
}
|
|
wsk.Spec.PodTemplate.Options.PodConfig.Values = newValues
|
|
return ContainSubstring("podConfig value %q is in use and cannot be removed", toBeRemoved)
|
|
},
|
|
},
|
|
{
|
|
description: "should accept removing an unused imageConfig value",
|
|
shouldSucceed: true,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
workspace: NewExampleWorkspace(workspaceName, namespaceName, workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
toBeRemoved := "redirect_step_1"
|
|
newValues := make([]kubefloworgv1beta1.ImageConfigValue, 0)
|
|
for _, value := range wsk.Spec.PodTemplate.Options.ImageConfig.Values {
|
|
if value.Id != toBeRemoved {
|
|
newValues = append(newValues, value)
|
|
}
|
|
}
|
|
wsk.Spec.PodTemplate.Options.ImageConfig.Values = newValues
|
|
return ContainSubstring("")
|
|
},
|
|
},
|
|
{
|
|
description: "should accept removing an unused podConfig value",
|
|
shouldSucceed: true,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
workspace: NewExampleWorkspace(workspaceName, namespaceName, workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
toBeRemoved := "redirect_step_1"
|
|
newValues := make([]kubefloworgv1beta1.PodConfigValue, 0)
|
|
for _, value := range wsk.Spec.PodTemplate.Options.PodConfig.Values {
|
|
if value.Id != toBeRemoved {
|
|
newValues = append(newValues, value)
|
|
}
|
|
}
|
|
wsk.Spec.PodTemplate.Options.PodConfig.Values = newValues
|
|
return ContainSubstring("")
|
|
},
|
|
},
|
|
{
|
|
description: "should reject removing the default imageConfig value",
|
|
shouldSucceed: false,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
toBeRemoved := "jupyterlab_scipy_190"
|
|
newValues := make([]kubefloworgv1beta1.ImageConfigValue, 0)
|
|
for _, value := range wsk.Spec.PodTemplate.Options.ImageConfig.Values {
|
|
if value.Id != toBeRemoved {
|
|
newValues = append(newValues, value)
|
|
}
|
|
}
|
|
wsk.Spec.PodTemplate.Options.ImageConfig.Values = newValues
|
|
return ContainSubstring("default imageConfig %q not found", toBeRemoved)
|
|
},
|
|
},
|
|
{
|
|
description: "should reject removing the default podConfig value",
|
|
shouldSucceed: false,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
workspace: NewExampleWorkspace(workspaceName, namespaceName, workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
toBeRemoved := "tiny_cpu"
|
|
newValues := make([]kubefloworgv1beta1.PodConfigValue, 0)
|
|
for _, value := range wsk.Spec.PodTemplate.Options.PodConfig.Values {
|
|
if value.Id != toBeRemoved {
|
|
newValues = append(newValues, value)
|
|
}
|
|
}
|
|
wsk.Spec.PodTemplate.Options.PodConfig.Values = newValues
|
|
return ContainSubstring("default podConfig %q not found", toBeRemoved)
|
|
},
|
|
},
|
|
{
|
|
description: "should reject removing the target of an imageConfig redirect",
|
|
shouldSucceed: false,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
toBeRemoved := "redirect_step_2"
|
|
newValues := make([]kubefloworgv1beta1.ImageConfigValue, 0)
|
|
for _, value := range wsk.Spec.PodTemplate.Options.ImageConfig.Values {
|
|
if value.Id != toBeRemoved {
|
|
newValues = append(newValues, value)
|
|
}
|
|
}
|
|
wsk.Spec.PodTemplate.Options.ImageConfig.Values = newValues
|
|
return ContainSubstring("target imageConfig %q does not exist", toBeRemoved)
|
|
},
|
|
},
|
|
{
|
|
description: "should reject removing the target of a podConfig redirect",
|
|
shouldSucceed: false,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
toBeRemoved := "redirect_step_2"
|
|
newValues := make([]kubefloworgv1beta1.PodConfigValue, 0)
|
|
for _, value := range wsk.Spec.PodTemplate.Options.PodConfig.Values {
|
|
if value.Id != toBeRemoved {
|
|
newValues = append(newValues, value)
|
|
}
|
|
}
|
|
wsk.Spec.PodTemplate.Options.PodConfig.Values = newValues
|
|
return ContainSubstring("target podConfig %q does not exist", toBeRemoved)
|
|
},
|
|
},
|
|
{
|
|
description: "should accept removing an entire imageConfig redirect chain",
|
|
shouldSucceed: true,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
toBeRemoved := map[string]bool{"redirect_step_1": true, "redirect_step_2": true, "redirect_step_3": true}
|
|
newValues := make([]kubefloworgv1beta1.ImageConfigValue, 0)
|
|
for _, value := range wsk.Spec.PodTemplate.Options.ImageConfig.Values {
|
|
if !toBeRemoved[value.Id] {
|
|
newValues = append(newValues, value)
|
|
}
|
|
}
|
|
wsk.Spec.PodTemplate.Options.ImageConfig.Values = newValues
|
|
return ContainSubstring("")
|
|
},
|
|
},
|
|
{
|
|
description: "should accept removing an entire podConfig redirect chain",
|
|
shouldSucceed: true,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
toBeRemoved := map[string]bool{"redirect_step_1": true, "redirect_step_2": true, "redirect_step_3": true}
|
|
newValues := make([]kubefloworgv1beta1.PodConfigValue, 0)
|
|
for _, value := range wsk.Spec.PodTemplate.Options.PodConfig.Values {
|
|
if !toBeRemoved[value.Id] {
|
|
newValues = append(newValues, value)
|
|
}
|
|
}
|
|
wsk.Spec.PodTemplate.Options.PodConfig.Values = newValues
|
|
return ContainSubstring("")
|
|
},
|
|
},
|
|
{
|
|
description: "should reject updating an imageConfig value to create a self-cycle",
|
|
shouldSucceed: false,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
valueId := wsk.Spec.PodTemplate.Options.ImageConfig.Values[1].Id
|
|
wsk.Spec.PodTemplate.Options.ImageConfig.Values[1].Redirect = &kubefloworgv1beta1.OptionRedirect{To: valueId}
|
|
return ContainSubstring("imageConfig redirect cycle detected: [%s]", valueId)
|
|
},
|
|
},
|
|
{
|
|
description: "should reject updating a podConfig value to create a 2-step cycle",
|
|
shouldSucceed: false,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
step1 := wsk.Spec.PodTemplate.Options.PodConfig.Values[0].Id
|
|
step2 := wsk.Spec.PodTemplate.Options.PodConfig.Values[1].Id
|
|
wsk.Spec.PodTemplate.Options.PodConfig.Values[0].Redirect = &kubefloworgv1beta1.OptionRedirect{To: step2}
|
|
wsk.Spec.PodTemplate.Options.PodConfig.Values[1].Redirect = &kubefloworgv1beta1.OptionRedirect{To: step1}
|
|
return ContainSubstring("podConfig redirect cycle detected: [") // there is no guarantee on which element will be first
|
|
},
|
|
},
|
|
{
|
|
description: "should reject updating an imageConfig to redirect to a non-existent value",
|
|
shouldSucceed: false,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
invalidTarget := "invalid_image_config"
|
|
wsk.Spec.PodTemplate.Options.ImageConfig.Values[1].Redirect = &kubefloworgv1beta1.OptionRedirect{To: invalidTarget}
|
|
return ContainSubstring("target imageConfig %q does not exist", invalidTarget)
|
|
},
|
|
},
|
|
{
|
|
description: "should reject updating a podConfig to redirect to a non-existent value",
|
|
shouldSucceed: false,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
invalidTarget := "invalid_pod_config"
|
|
wsk.Spec.PodTemplate.Options.PodConfig.Values[0].Redirect = &kubefloworgv1beta1.OptionRedirect{To: invalidTarget}
|
|
return ContainSubstring("target podConfig %q does not exist", invalidTarget)
|
|
},
|
|
},
|
|
{
|
|
description: "should reject updating the default imageConfig value to a non-existent value",
|
|
shouldSucceed: false,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
invalidDefault := "invalid_image_config"
|
|
wsk.Spec.PodTemplate.Options.ImageConfig.Spawner.Default = invalidDefault
|
|
return ContainSubstring("default imageConfig %q not found", invalidDefault)
|
|
},
|
|
},
|
|
{
|
|
description: "should reject updating the default podConfig value to a non-existent value",
|
|
shouldSucceed: false,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
invalidDefault := "invalid_pod_config"
|
|
wsk.Spec.PodTemplate.Options.PodConfig.Spawner.Default = invalidDefault
|
|
return ContainSubstring("default podConfig %q not found", invalidDefault)
|
|
},
|
|
},
|
|
{
|
|
description: "should reject updating an imageConfig to have duplicate ports",
|
|
shouldSucceed: false,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
duplicatePortNumber := int32(8888)
|
|
wsk.Spec.PodTemplate.Options.ImageConfig.Values[1].Spec.Ports = []kubefloworgv1beta1.ImagePort{
|
|
{
|
|
Id: "jupyterlab",
|
|
DisplayName: "JupyterLab",
|
|
Port: duplicatePortNumber,
|
|
Protocol: "HTTP",
|
|
},
|
|
{
|
|
Id: "jupyterlab2",
|
|
DisplayName: "JupyterLab2",
|
|
Port: duplicatePortNumber,
|
|
Protocol: "HTTP",
|
|
},
|
|
}
|
|
return ContainSubstring("port %d is defined more than once", duplicatePortNumber)
|
|
},
|
|
},
|
|
{
|
|
description: "should reject updating an extraEnv[].value to an invalid Go template",
|
|
shouldSucceed: false,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
invalidValue := `{{ httpPathPrefix "jupyterlab" }`
|
|
wsk.Spec.PodTemplate.ExtraEnv[0].Value = invalidValue
|
|
return ContainSubstring("failed to parse template %q", invalidValue)
|
|
},
|
|
},
|
|
{
|
|
description: "should accept updating an extraEnv[].value to a valid Go template",
|
|
shouldSucceed: true,
|
|
|
|
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
|
|
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
|
|
wsk.Spec.PodTemplate.ExtraEnv[0].Value = `{{ httpPathPrefix "jupyterlab" }}`
|
|
return ContainSubstring("")
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
It(tc.description, func() {
|
|
if tc.workspaceKind == nil {
|
|
Fail("invalid test case definition: workspaceKind is required")
|
|
}
|
|
|
|
By("creating the WorkspaceKind")
|
|
// NOTE: cleanup is handled in the AfterEach()
|
|
Expect(k8sClient.Create(ctx, tc.workspaceKind)).To(Succeed())
|
|
|
|
By("retrieving the WorkspaceKind")
|
|
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{}
|
|
Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(tc.workspaceKind), workspaceKind)).To(Succeed())
|
|
|
|
if tc.workspace != nil {
|
|
By("creating the Workspace")
|
|
// NOTE: cleanup is handled in the AfterEach()
|
|
Expect(k8sClient.Create(ctx, tc.workspace)).To(Succeed())
|
|
|
|
By("retrieving the Workspace")
|
|
workspace := &kubefloworgv1beta1.Workspace{}
|
|
Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(tc.workspace), workspace)).To(Succeed())
|
|
}
|
|
|
|
By("updating the WorkspaceKind")
|
|
patch := client.MergeFrom(workspaceKind.DeepCopy())
|
|
modifiedWorkspaceKind := workspaceKind.DeepCopy()
|
|
errMatcher := tc.modifyKindFn(modifiedWorkspaceKind)
|
|
err := k8sClient.Patch(ctx, modifiedWorkspaceKind, patch)
|
|
if tc.shouldSucceed {
|
|
Expect(err).To(Succeed())
|
|
} else {
|
|
Expect(err).NotTo(Succeed())
|
|
Expect(err.Error()).To(errMatcher)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
})
|