Skip to content

Commit 5170f6e

Browse files
committed
add validation webhook for podtemplate.ports
Signed-off-by: Harshad Reddy Nalla <[email protected]>
1 parent 8b24a9a commit 5170f6e

File tree

3 files changed

+133
-0
lines changed

3 files changed

+133
-0
lines changed

workspaces/controller/internal/webhook/suite_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,9 @@ func NewExampleWorkspaceKind(name string) *kubefloworgv1beta1.WorkspaceKind {
220220
},
221221
},
222222
},
223+
{
224+
PortId: "my_port",
225+
},
223226
},
224227
ExtraEnv: []v1.EnvVar{
225228
{
@@ -618,6 +621,38 @@ func NewExampleWorkspaceKindWithDuplicatePorts(name string) *kubefloworgv1beta1.
618621
return workspaceKind
619622
}
620623

624+
// NewExampleWorkspaceKindWithEmptyPortsArrayInPodTemplate returns a WorkspaceKind with an empty ports array in podTemplate.ports.
625+
func NewExampleWorkspaceKindWithEmptyPortsArrayInPodTemplate(name string) *kubefloworgv1beta1.WorkspaceKind {
626+
workspaceKind := NewExampleWorkspaceKind(name)
627+
workspaceKind.Spec.PodTemplate.Ports = []kubefloworgv1beta1.WorkspaceKindPort{}
628+
return workspaceKind
629+
}
630+
631+
// NewExampleWorkspaceKindWithDuplicatePortsInPodTemplate returns a WorkspaceKind with duplicate ports in podTemplate.ports.
632+
func NewExampleWorkspaceKindWithDuplicatePortsInPodTemplate(name string) *kubefloworgv1beta1.WorkspaceKind {
633+
workspaceKind := NewExampleWorkspaceKind(name)
634+
workspaceKind.Spec.PodTemplate.Ports = []kubefloworgv1beta1.WorkspaceKindPort{
635+
{
636+
PortId: "jupyterlab",
637+
},
638+
{
639+
PortId: "jupyterlab",
640+
},
641+
}
642+
return workspaceKind
643+
}
644+
645+
// NewExampleWorkspaceKindWithNonExistentPortIdInImageConfig returns a WorkspaceKind with a non-existent portId in imageConfig.ports.
646+
func NewExampleWorkspaceKindWithNonExistentPortIdInImageConfig(name string) *kubefloworgv1beta1.WorkspaceKind {
647+
workspaceKind := NewExampleWorkspaceKind(name)
648+
workspaceKind.Spec.PodTemplate.Ports = []kubefloworgv1beta1.WorkspaceKindPort{
649+
{
650+
PortId: "non-existent-port-id",
651+
},
652+
}
653+
return workspaceKind
654+
}
655+
621656
// NewExampleWorkspaceKindWithInvalidExtraEnvValue returns a WorkspaceKind with an invalid extraEnv value.
622657
func NewExampleWorkspaceKindWithInvalidExtraEnvValue(name string) *kubefloworgv1beta1.WorkspaceKind {
623658
workspaceKind := NewExampleWorkspaceKind(name)

workspaces/controller/internal/webhook/workspacekind_webhook.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ func (v *WorkspaceKindValidator) ValidateCreate(ctx context.Context, obj runtime
7878
// validate the extra environment variables
7979
allErrs = append(allErrs, validateExtraEnv(workspaceKind)...)
8080

81+
// validate the ports configuration
82+
allErrs = append(allErrs, validatePorts(workspaceKind)...)
83+
8184
// generate helper maps for imageConfig values
8285
imageConfigIdMap := make(map[string]kubefloworgv1beta1.ImageConfigValue)
8386
imageConfigRedirectMap := make(map[string]string)
@@ -156,6 +159,11 @@ func (v *WorkspaceKindValidator) ValidateUpdate(ctx context.Context, oldObj, new
156159
allErrs = append(allErrs, validateExtraEnv(newWorkspaceKind)...)
157160
}
158161

162+
// validate the ports configuration
163+
if !equality.Semantic.DeepEqual(newWorkspaceKind.Spec.PodTemplate.Ports, oldWorkspaceKind.Spec.PodTemplate.Ports) {
164+
allErrs = append(allErrs, validatePorts(newWorkspaceKind)...)
165+
}
166+
159167
// calculate changes to imageConfig values
160168
var shouldValidateImageConfigRedirects = false
161169
toValidateImageConfigIds := make(map[string]bool)
@@ -516,6 +524,61 @@ func (v *WorkspaceKindValidator) validatePodTemplatePodMetadata(workspaceKind *k
516524
return errs
517525
}
518526

527+
// validatePorts validates the ports in podTemplate.ports of WorkspaceKind
528+
func validatePorts(workspaceKind *kubefloworgv1beta1.WorkspaceKind) []*field.Error {
529+
var errs []*field.Error
530+
531+
ports := workspaceKind.Spec.PodTemplate.Ports
532+
portsPath := field.NewPath("spec", "podTemplate", "ports")
533+
534+
// if no ports are defined, we can skip validation
535+
if len(ports) == 0 {
536+
return errs
537+
}
538+
539+
// collect all available port IDs from imageConfig values
540+
availablePortIds := make(map[string]bool)
541+
for _, imageConfigValue := range workspaceKind.Spec.PodTemplate.Options.ImageConfig.Values {
542+
for _, imagePort := range imageConfigValue.Spec.Ports {
543+
availablePortIds[imagePort.Id] = true
544+
}
545+
}
546+
547+
// track seen port IDs to ensure uniqueness
548+
seenPortIds := make(map[string]bool)
549+
550+
// validate each port in the ports array
551+
for i, port := range ports {
552+
portId := port.PortId
553+
portPath := portsPath.Index(i)
554+
portIdPath := portPath.Child("portId")
555+
556+
// validate that portId is not empty (should be caught by CRD validation, but be safe)
557+
if portId == "" {
558+
errs = append(errs, field.Required(portIdPath, "portId is required"))
559+
continue
560+
}
561+
562+
// validate that each port has a unique portId
563+
if seenPortIds[portId] {
564+
errs = append(errs, field.Duplicate(portIdPath, portId))
565+
} else {
566+
seenPortIds[portId] = true
567+
}
568+
569+
// validate that the portId references a valid port from imageConfig
570+
if !availablePortIds[portId] {
571+
errs = append(errs, field.Invalid(portIdPath, portId,
572+
fmt.Sprintf("portId %q does not match any port defined in imageConfig values", portId)))
573+
}
574+
575+
// httpProxy is optional, so no validation needed for nil
576+
// The structure validation is handled by the CRD schema
577+
}
578+
579+
return errs
580+
}
581+
519582
// validateExtraEnv validates the extra environment variables in a WorkspaceKind
520583
func validateExtraEnv(workspaceKind *kubefloworgv1beta1.WorkspaceKind) []*field.Error {
521584
var errs []*field.Error

workspaces/controller/internal/webhook/workspacekind_webhook_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,21 @@ var _ = Describe("WorkspaceKind Webhook", func() {
9898
workspaceKind: NewExampleWorkspaceKindWithDuplicatePorts("wsk-webhook-create--image-config-duplicate-ports"),
9999
shouldSucceed: false,
100100
},
101+
{
102+
description: "should reject creation with empty ports array in podTemplate",
103+
workspaceKind: NewExampleWorkspaceKindWithEmptyPortsArrayInPodTemplate("wsk-webhook-create--pod-template-empty-ports-array"),
104+
shouldSucceed: true,
105+
},
106+
{
107+
description: "should reject creation with duplicate ports in podTemplate.ports",
108+
workspaceKind: NewExampleWorkspaceKindWithDuplicatePortsInPodTemplate("wsk-webhook-create--pod-template-duplicate-portids"),
109+
shouldSucceed: false,
110+
},
111+
{
112+
description: "should reject creation with non-existent portId in imageConfig.ports",
113+
workspaceKind: NewExampleWorkspaceKindWithNonExistentPortIdInImageConfig("wsk-webhook-create--image-config-non-existent-portid"),
114+
shouldSucceed: false,
115+
},
101116
{
102117
description: "should reject creation if extraEnv[].value is not a valid Go template",
103118
workspaceKind: NewExampleWorkspaceKindWithInvalidExtraEnvValue("wsk-webhook-create--extra-invalid-env-value"),
@@ -508,6 +523,26 @@ var _ = Describe("WorkspaceKind Webhook", func() {
508523
return ContainSubstring("port %d is defined more than once", duplicatePortNumber)
509524
},
510525
},
526+
{
527+
description: "should reject updating a portId in podTemplate.ports to a duplicate portId",
528+
shouldSucceed: false,
529+
530+
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
531+
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
532+
wsk.Spec.PodTemplate.Ports[1].PortId = "jupyterlab"
533+
return ContainSubstring("Duplicate value: %q", "jupyterlab")
534+
},
535+
},
536+
{
537+
description: "should reject updating a portId in podTemplate.ports to a non-existent portId in imageConfig.ports",
538+
shouldSucceed: false,
539+
540+
workspaceKind: NewExampleWorkspaceKind(workspaceKindName),
541+
modifyKindFn: func(wsk *kubefloworgv1beta1.WorkspaceKind) gomegaTypes.GomegaMatcher {
542+
wsk.Spec.PodTemplate.Ports[0].PortId = "non-existent-port-id"
543+
return ContainSubstring("portId %q does not match any port defined in imageConfig values", "non-existent-port-id")
544+
},
545+
},
511546
{
512547
description: "should reject updating a podMetadata.labels key to an invalid value",
513548
shouldSucceed: false,

0 commit comments

Comments
 (0)