Skip to content

Conversation

harshad16
Copy link

@harshad16 harshad16 commented Jul 29, 2025

related: #37

This PR adds spec.podTemplate.ports[] to workspaceKind CRD, which lets users include ports httpproxy setting for their workspaces.

WorkspaceKind CRD changes

spec:
  podTemplate:
    ports:
      - id: "jupyterlab"
        portocol: HTTP
        displayname: "Jupyterlab"
        httpProxy: {}

Following changes are included:

  • moved protocol from imageconfig.spec.ports to podtemplates.ports
  • included the podtemplates.ports with defaultdisplayname
  • add validation webhook for podtemplate.ports
  • update the sample workspacekind with ports reference
  • referencing same id for portid in imageconfig and podtemplate.ports

These changes would be consider while setting the routing for proper traffic controller/routing to the pods.

Copy link

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign thesuperzapper for approval. For more information see the Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@google-oss-prow google-oss-prow bot added area/controller area - related to controller components area/v2 area - version - kubeflow notebooks v2 labels Jul 29, 2025
@harshad16 harshad16 marked this pull request as ready for review July 31, 2025 05:14
@thesuperzapper
Copy link
Member

/ok-to-test

Copy link
Contributor

@andyatmiami andyatmiami left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// +kubebuilder:validation:Optional
HTTPProxy *HTTPProxy `json:"httpProxy,omitempty"`
Ports []WorkspaceKindPort `json:"ports,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity - what is the practical purpose of defining a WorkspaceKind with no Ports ? Why would someone want to do that ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great question , we should rethink this 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should require at least one port, just so that the frontend does not have to deal with the possibility of a workspace with no ports.

+kubebuilder:validation:MinItems:=1

@harshad16 harshad16 force-pushed the port-in-wsk-podtemplate branch from 5170f6e to 438e485 Compare August 6, 2025 18:56
@google-oss-prow google-oss-prow bot added the area/backend area - related to backend components label Aug 6, 2025
@harshad16 harshad16 force-pushed the port-in-wsk-podtemplate branch from 438e485 to 3239dde Compare August 6, 2025 19:40
@andyatmiami
Copy link
Contributor

/ok-to-test

@andyatmiami
Copy link
Contributor

/lgtm

testing these changes on a cluster and was able to:

  • create a workspacekind (using samples/)
  • create a workspace referencing the workspacekind (using samples/)
  • view the YAML representation of workspacekind and see the ports: changes
 $ kubectl get workspacekinds.kubeflow.org/jupyterlab -o yaml
apiVersion: kubeflow.org/v1beta1
kind: WorkspaceKind
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"kubeflow.org/v1beta1","kind":"WorkspaceKind","metadata":{"annotations":{},"name":"jupyterlab"},"spec":{"podTemplate":{"containerSecurityContext":{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"runAsNonRoot":true},"culling":{"activityProbe":{"jupyter":{"lastActivity":true}},"enabled":true,"maxInactiveSeconds":86400},"extraEnv":[{"name":"NB_PREFIX","value":"{{ httpPathPrefix \"jupyterlab\" }}"}],"extraVolumeMounts":[{"mountPath":"/dev/shm","name":"dshm"}],"extraVolumes":[{"emptyDir":{"medium":"Memory"},"name":"dshm"}],"options":{"imageConfig":{"spawner":{"default":"jupyterlab_scipy_190"},"values":[{"id":"jupyterlab_scipy_180","redirect":{"message":{"level":"Info","text":"This update will change..."},"to":"jupyterlab_scipy_190"},"spawner":{"description":"JupyterLab, with SciPy Packages","displayName":"jupyter-scipy:v1.8.0","hidden":true,"labels":[{"key":"python_version","value":"3.11"}]},"spec":{"image":"ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-scipy:v1.8.0","imagePullPolicy":"IfNotPresent","ports":[{"displayName":"JupyterLab","id":"jupyterlab","port":8888,"protocol":"HTTP"}]}},{"id":"jupyterlab_scipy_190","spawner":{"description":"JupyterLab, with SciPy Packages","displayName":"jupyter-scipy:v1.9.0","labels":[{"key":"python_version","value":"3.11"}]},"spec":{"image":"ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-scipy:v1.9.0","imagePullPolicy":"IfNotPresent","ports":[{"displayName":"JupyterLab","id":"jupyterlab","port":8888,"protocol":"HTTP"}]}}]},"podConfig":{"spawner":{"default":"tiny_cpu"},"values":[{"id":"tiny_cpu","spawner":{"description":"Pod with 0.1 CPU, 128 Mb RAM","displayName":"Tiny CPU","labels":[{"key":"cpu","value":"100m"},{"key":"memory","value":"128Mi"}]},"spec":{"resources":{"requests":{"cpu":"100m","memory":"128Mi"}}}},{"id":"small_cpu","spawner":{"description":"Pod with 1 CPU, 2 GB RAM","displayName":"Small CPU","hidden":false,"labels":[{"key":"cpu","value":"1000m"},{"key":"memory","value":"2Gi"}]},"spec":{"affinity":{},"nodeSelector":{},"resources":{"requests":{"cpu":"1000m","memory":"2Gi"}},"tolerations":[]}},{"id":"big_gpu","spawner":{"description":"Pod with 4 CPU, 16 GB RAM, and 1 GPU","displayName":"Big GPU","hidden":false,"labels":[{"key":"cpu","value":"4000m"},{"key":"memory","value":"16Gi"},{"key":"gpu","value":"1"}]},"spec":{"affinity":{},"nodeSelector":{},"resources":{"limits":{"nvidia.com/gpu":1},"requests":{"cpu":"4000m","memory":"16Gi"}},"tolerations":[{"effect":"NoSchedule","key":"nvidia.com/gpu","operator":"Exists"}]}}]}},"podMetadata":{"annotations":{"my-workspace-kind-annotation":"my-value"},"labels":{"my-workspace-kind-label":"my-value"}},"ports":[{"httpProxy":{"removePathPrefix":false,"requestHeaders":{}},"portId":"jupyterlab"}],"probes":null,"securityContext":{"fsGroup":100},"serviceAccount":{"name":"default-editor"},"volumeMounts":{"home":"/home/jovyan"}},"spawner":{"deprecated":false,"deprecationMessage":"This WorkspaceKind will be removed on 20XX-XX-XX, please use another WorkspaceKind.","description":"A Workspace which runs JupyterLab in a Pod","displayName":"JupyterLab Notebook","hidden":false,"icon":{"url":"https://jupyter.org/assets/favicons/apple-touch-icon-152x152.png"},"logo":{"url":"https://upload.wikimedia.org/wikipedia/commons/3/38/Jupyter_logo.svg"}}}}
  creationTimestamp: "2025-08-06T21:02:15Z"
  finalizers:
  - notebooks.kubeflow.org/workspacekind-protection
  generation: 2
  name: jupyterlab
  resourceVersion: "31521"
  uid: 22488214-4fa7-410d-b41c-50d8624c5134
spec:
  podTemplate:
    containerSecurityContext:
      allowPrivilegeEscalation: false
      capabilities:
        drop:
        - ALL
      runAsNonRoot: true
    culling:
      activityProbe:
        jupyter:
          lastActivity: true
      enabled: true
      maxInactiveSeconds: 86400
    extraEnv:
    - name: NB_PREFIX
      value: '{{ httpPathPrefix "jupyterlab" }}'
    extraVolumeMounts:
    - mountPath: /dev/shm
      name: dshm
    extraVolumes:
    - emptyDir:
        medium: Memory
      name: dshm
    options:
      imageConfig:
        spawner:
          default: jupyterlab_scipy_190
        values:
        - id: jupyterlab_scipy_180
          redirect:
            message:
              level: Info
              text: This update will change...
            to: jupyterlab_scipy_190
          spawner:
            description: JupyterLab, with SciPy Packages
            displayName: jupyter-scipy:v1.8.0
            hidden: true
            labels:
            - key: python_version
              value: "3.11"
          spec:
            image: ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-scipy:v1.8.0
            imagePullPolicy: IfNotPresent
            ports:
            - displayName: JupyterLab
              id: jupyterlab
              port: 8888
              protocol: HTTP
        - id: jupyterlab_scipy_190
          spawner:
            description: JupyterLab, with SciPy Packages
            displayName: jupyter-scipy:v1.9.0
            hidden: false
            labels:
            - key: python_version
              value: "3.11"
          spec:
            image: ghcr.io/kubeflow/kubeflow/notebook-servers/jupyter-scipy:v1.9.0
            imagePullPolicy: IfNotPresent
            ports:
            - displayName: JupyterLab
              id: jupyterlab
              port: 8888
              protocol: HTTP
      podConfig:
        spawner:
          default: tiny_cpu
        values:
        - id: tiny_cpu
          spawner:
            description: Pod with 0.1 CPU, 128 Mb RAM
            displayName: Tiny CPU
            hidden: false
            labels:
            - key: cpu
              value: 100m
            - key: memory
              value: 128Mi
          spec:
            resources:
              requests:
                cpu: 100m
                memory: 128Mi
        - id: small_cpu
          spawner:
            description: Pod with 1 CPU, 2 GB RAM
            displayName: Small CPU
            hidden: false
            labels:
            - key: cpu
              value: 1000m
            - key: memory
              value: 2Gi
          spec:
            affinity: {}
            resources:
              requests:
                cpu: "1"
                memory: 2Gi
        - id: big_gpu
          spawner:
            description: Pod with 4 CPU, 16 GB RAM, and 1 GPU
            displayName: Big GPU
            hidden: false
            labels:
            - key: cpu
              value: 4000m
            - key: memory
              value: 16Gi
            - key: gpu
              value: "1"
          spec:
            affinity: {}
            resources:
              limits:
                nvidia.com/gpu: "1"
              requests:
                cpu: "4"
                memory: 16Gi
            tolerations:
            - effect: NoSchedule
              key: nvidia.com/gpu
              operator: Exists
    podMetadata:
      annotations:
        my-workspace-kind-annotation: my-value
      labels:
        my-workspace-kind-label: my-value
    ports:
    - httpProxy:
        removePathPrefix: false
        requestHeaders: {}
      portId: jupyterlab
    securityContext:
      fsGroup: 100
    serviceAccount:
      name: default-editor
    volumeMounts:
      home: /home/jovyan
  spawner:
    deprecated: false
    deprecationMessage: This WorkspaceKind will be removed on 20XX-XX-XX, please use
      another WorkspaceKind.
    description: A Workspace which runs JupyterLab in a Pod
    displayName: JupyterLab Notebook
    hidden: false
    icon:
      url: https://jupyter.org/assets/favicons/apple-touch-icon-152x152.png
    logo:
      url: https://upload.wikimedia.org/wikipedia/commons/3/38/Jupyter_logo.svg
status:
  podTemplateOptions:
    imageConfig:
    - id: jupyterlab_scipy_180
      workspaces: 0
    - id: jupyterlab_scipy_190
      workspaces: 1
    podConfig:
    - id: tiny_cpu
      workspaces: 1
    - id: small_cpu
      workspaces: 0
    - id: big_gpu
      workspaces: 0
  workspaces: 1

@google-oss-prow google-oss-prow bot added the lgtm label Aug 6, 2025
Comment on lines 32 to 33
// PortId represents a port identifier
// - this is NOT used as the Container or Service port name, but as part of the HTTP path
// - this is used to reference the port in the `imageconfig` ports.[].id
// - this is also used to reference the port in the podtemplate ports.[].portId
//
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// PortId represents a port identifier
// - this is NOT used as the Container or Service port name, but as part of the HTTP path
// - this is used to reference the port in the `imageconfig` ports.[].id
// - this is also used to reference the port in the podtemplate ports.[].portId
//
// PortId the id of the port

// the id of the port
// - identifier for the port in `imageconfig` ports.[].id
// +kubebuilder:example="jupyterlab"
PortId PortId `json:"portId"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably lets just make this id, because its kind of implied by the ports list.

 - moved protocal from imageconfig.spec.ports to podtemplates.ports
 - included the podtemplates.ports with defaultdisplayname
 - add validation webhook for podtemplate.ports
 - update the sample workspacekind with ports reference
 - referencing same id for portid in imageconfig and podtemplate.ports

Signed-off-by: Harshad Reddy Nalla <[email protected]>
@harshad16 harshad16 force-pushed the port-in-wsk-podtemplate branch from 9532f2f to c8382d8 Compare September 11, 2025 13:00
@andyatmiami
Copy link
Contributor

/lgtm

Testing Methodology

  1. using a kind cluster created by a script I have

  2. ➜ controller/ git:((HEAD detached at harshad/port-in-wsk-podtemplate)) $ gmake

  3. ➜ controller/ git:((HEAD detached at harshad/port-in-wsk-podtemplate)) $ gmake install

  4. ➜ controller/ git:((HEAD detached at harshad/port-in-wsk-podtemplate)) $ gmake test

  5. ➜ controller/ git:((HEAD detached at harshad/port-in-wsk-podtemplate)) $ DOCKER_BUILDKIT=0 gmake docker-build IMG=quay.io/rh-ee-astonebe/kubeflow-notebooks-v2:controller-ports-harshad-review

  6. ➜ controller/ git:((HEAD detached at harshad/port-in-wsk-podtemplate)) $ KIND_EXPERIMENTAL_PROVIDER=podman kind load docker-image quay.io/rh-ee-astonebe/kubeflow-notebooks-v2:controller-ports-harshad-review --name kubeflow

  7. ➜ controller/ git:((HEAD detached at harshad/port-in-wsk-podtemplate)) $ kubectl -n workspace-controller-system edit deployment.apps/workspace-controller-controller-manager

    • replace container image with one built off this branch
  8. ➜ controller/ git:((HEAD detached at harshad/port-in-wsk-podtemplate)) $ kubectl -n workspace-controller-system get deployment.apps/workspace-controller-controller-manager -o yaml

    • verify desired image in place and deployment healthy
  9. ➜ workspaces/ git:((HEAD detached at harshad/port-in-wsk-podtemplate)) $ cd controller/config/samples/common

  10. ➜ common/ git:((HEAD detached at harshad/port-in-wsk-podtemplate)) $ kubectl -n kubeflow-default-profile apply -f workspace_home_pvc.yaml

  11. ➜ common/ git:((HEAD detached at harshad/port-in-wsk-podtemplate)) $ kubectl -n kubeflow-default-profile apply -f workspace_data_pvc.yaml

  12. ➜ common/ git:((HEAD detached at harshad/port-in-wsk-podtemplate)) $ cd ~/Development/Test/notebooks-v2/manifests/harshad-ports

    • directory with manifests:
      • /samples for valid workspacekind + workspace
      • modified samples/ workspacekind that has:
        • unreferenced ports element (i.e. no image using it)
        • invalid ports.id reference in imageConfig
  13. ➜ harshad-ports/ $ kubectl -n kubeflow-default-profile apply -f jupyterlab_v1beta1_workspacekind.yaml

  14. ➜ harshad-ports/ $ kubectl -n kubeflow-default-profile apply -f jupyterlab_v1beta1_workspace.yaml

  15. ➜ harshad-ports/ $ kubectl get all -n kubeflow-default-profile

    • confirm pod created from Workspace CR creation
  16. ➜ harshad-ports/ $ kubectl -n kubeflow-default-profile apply -f jupyterlab_v1beta1_workspacekind-bad.yaml

    • confirm rejected by API server
    The WorkspaceKind "jupyterlab" is invalid: spec.podTemplate.ports: Invalid value: "does-not-exist": port ID "does-not-exist" is referenced in imageConfig but not defined in ports
    

@google-oss-prow google-oss-prow bot added the lgtm label Sep 11, 2025
Copy link
Member

@thesuperzapper thesuperzapper left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @harshad16 here are comments.

@@ -340,12 +340,9 @@ func buildServices(ws *kubefloworgv1beta1.Workspace, imageConfigValue *kubeflowo
services := make([]Service, len(imageConfigValue.Spec.Ports))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to first build a map from portId -> port config (from the outer workspace kind ports), so that we can look it up when processing each of the ports.

@@ -340,12 +340,9 @@ func buildServices(ws *kubefloworgv1beta1.Workspace, imageConfigValue *kubeflowo
services := make([]Service, len(imageConfigValue.Spec.Ports))
for i := range imageConfigValue.Spec.Ports {
port := imageConfigValue.Spec.Ports[i]
switch port.Protocol { //nolint:gocritic
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should keep this switch statement, but instead switch on the protocol from the outer workspace kind ports list.

HttpPath: fmt.Sprintf("/workspace/%s/%s/%s/", ws.Namespace, ws.Name, port.Id),
}
services[i].HttpService = &HttpService{
DisplayName: *port.DisplayName,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will null pointer deref error, you should use something like ptr.Deref(port.DisplayName, default DisplayNameForThisPort)

@@ -342,9 +348,8 @@ func NewExampleWorkspaceKind(name string) *kubefloworgv1beta1.WorkspaceKind {
Ports: []kubefloworgv1beta1.ImagePort{
{
Id: "jupyterlab",
DisplayName: "JupyterLab",
DisplayName: ptr.To("JupyterLab"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be testing with at least one of these being nil, so that we use the default.

// +kubebuilder:validation:Optional
HTTPProxy *HTTPProxy `json:"httpProxy,omitempty"`
Ports []WorkspaceKindPort `json:"ports,omitempty"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should require at least one port, just so that the frontend does not have to deal with the possibility of a workspace with no ports.

+kubebuilder:validation:MinItems:=1

@@ -317,9 +323,8 @@ func NewExampleWorkspaceKind1(name string) *kubefloworgv1beta1.WorkspaceKind {
Ports: []kubefloworgv1beta1.ImagePort{
{
Id: "jupyterlab",
DisplayName: "JupyterLab",
DisplayName: ptr.To("JupyterLab"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For one of these, we should have it empty to use the default.

@@ -650,7 +650,7 @@ func generateStatefulSet(workspace *kubefloworgv1beta1.Workspace, workspaceKind
seenPorts[port.Port] = true

// NOTE: we construct this map for use in the go string templates
containerPortsIdMap[port.Id] = port
containerPortsIdMap[string(port.Id)] = port
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implies that you need to update the type of containerPortsIdMap to map[PortId]kubefloworgv1beta1.ImagePort, note, this might mean we need to change this other places which use this map.

@@ -516,6 +537,59 @@ func (v *WorkspaceKindValidator) validatePodTemplatePodMetadata(workspaceKind *k
return errs
}

// validatePorts validates the ports in podTemplate.ports of WorkspaceKind
func validatePorts(workspaceKind *kubefloworgv1beta1.WorkspaceKind) []*field.Error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove this function and just do it in validateImageConfigValue.

  1. Define a map from which to look up the ports which are defined under spec.podTemplate.ports by ID:
    • Note, update validateImageConfigValue to take in this map rather than doing it each time.
    • The only thing we might want to validate is that HTTPProxy is only set if protocol is HTTP.
    • You don't need to validate them in this loop because we already have min characters etc.
  2. Add the existence check to the existing loop over ports in validateImageConfigValue
    • The only validation we need is if the port "id" exists in the previous map.
    • When we encounter one which does not exist, the "path" of the error should look like spec.podTemplate.options.imageConfig.values[<value_id>].spec.ports[<port_id>].id with an error like missing from spec.podTemplate.ports

@@ -78,6 +78,9 @@ func (v *WorkspaceKindValidator) ValidateCreate(ctx context.Context, obj runtime
// validate the extra environment variables
allErrs = append(allErrs, validateExtraEnv(workspaceKind)...)

// validate the ports configuration
allErrs = append(allErrs, validatePorts(workspaceKind)...)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this and instead only validate the ports in validateImageConfigValue.

@@ -156,6 +159,9 @@ func (v *WorkspaceKindValidator) ValidateUpdate(ctx context.Context, oldObj, new
allErrs = append(allErrs, validateExtraEnv(newWorkspaceKind)...)
}

// validate the ports configuration
shouldValidatePorts := !equality.Semantic.DeepEqual(newWorkspaceKind.Spec.PodTemplate.Ports, oldWorkspaceKind.Spec.PodTemplate.Ports)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's structure this a little different and name it shouldValidateAllImageConfigValues:

// if the ports config changed, we need to validate all image config vaules again
var shouldValidateAllImageConfigValues bool
shouldValidateAllImageConfigValues = !equality.Semantic.DeepEqual(newWorkspaceKind.Spec.PodTemplate.Ports, oldWorkspaceKind.Spec.PodTemplate.Ports)

This then should cause all imageConfig values to be true in toValidateImageConfigIds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/backend area - related to backend components area/controller area - related to controller components area/v2 area - version - kubeflow notebooks v2 ok-to-test size/XL
Projects
Status: Needs Triage
Development

Successfully merging this pull request may close these issues.

3 participants