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"
        protocol: 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.

@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.

// ports that the container listens on
// +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

@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.

return nil
}

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.

Copy link
Author

Choose a reason for hiding this comment

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

Made the changes accordingly
thanks

// ports that the container listens on
// +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

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 ask for approval from thesuperzapper. 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

@harshad16 harshad16 force-pushed the port-in-wsk-podtemplate branch 3 times, most recently from 5d88a27 to 30d1fac Compare September 16, 2025 19:26
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.

A couple (minor) last tweaks being suggested here. However, none of them are really functionality impacting - so I will note that I also tested this code against a kind cluster (making sure I updated both the controller AND backend deployments - and I can happily confirm:

  • creating workspace kinds + workspaces through kubectl works
  • creating workspace kinds + workspaces through frontend UI also work

Additionally confirmed common negative scenarios as well as ensuring "falling back" to the DefaultDisplayName is handled appropriately.

@harshad16 harshad16 force-pushed the port-in-wsk-podtemplate branch from 30d1fac to 689e7bc Compare September 25, 2025 06:31
 - 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 689e7bc to dffa3a7 Compare September 25, 2025 06:43
@andyatmiami
Copy link
Contributor

/lgtm

@google-oss-prow google-oss-prow bot added the lgtm label Sep 25, 2025
Comment on lines +94 to +97
var wskPodTemplatePorts []kubefloworgv1beta1.WorkspaceKindPort
if wskExists(wsk) {
wskPodTemplatePorts = wsk.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.

This should b a map from portId to port config so we can look it up by that in the buildServices section.

Same as comment https://github.com/kubeflow/notebooks/pull/507/files#r2342214467 from the last review.

// http proxy configs (MUTABLE)
// +kubebuilder:validation:Optional
HTTPProxy *HTTPProxy `json:"httpProxy,omitempty"`
// ports that the container listens on
Copy link
Member

Choose a reason for hiding this comment

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

This is very important to document for users as it is used in kubectl explain to explain what this field is:

Suggested change
// ports that the container listens on
// port definitions which can be referenced in image config values
// - think of port definitions as the "types" of services which could be provided by a specific image
// - a port definition has a common id (URL path) for consistency if the listening TCP port changes
// - ports are referenced in image config values by their `id` and their definition here establishes
// their protocol type, and default display name in the ui

Comment on lines +32 to +33
// PortId the id of the port
//
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 the id of the port
//
// an id of a port

// +kubebuilder:example:="HTTP"
Protocol ImagePortProtocol `json:"protocol"`

// the display name of the port
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
// the display name of the port
// the default display name of the port
// - note, this can be overridden on a per image config value basis

// +kubebuilder:validation:MinLength:=2
// +kubebuilder:validation:MaxLength:=64
// +kubebuilder:example:="JupyterLab"
DefaultDisplayName string `json:"displayName"`
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 include "default" so it's clear that this is override-able per port:

Suggested change
DefaultDisplayName string `json:"displayName"`
DefaultDisplayName string `json:"defaultDisplayName"`

## - this only works if the application serves RELATIVE URLs for its assets
##
removePathPrefix: false
## port configs (MUTABLE)
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
## port configs (MUTABLE)
## port definitions which can be referenced in image config values (MUTABLE)
## - think of port definitions as the "types" of services which could be provided by a specific image
## - a port definition has a common id (URL path) for consistency if the listening TCP port changes
## - ports are referenced in image config values by their `id` and their definition here establishes
## their protocol type, and default display name in the ui
##

Comment on lines +140 to +143

## the list of ports that the Workspace exposes
## configs apply to a single port
## portId is the identifier for the port in `imageconfig` ports.[].id
Copy link
Member

Choose a reason for hiding this comment

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

I think this part is explained by the above ports doc string:

Suggested change
## the list of ports that the Workspace exposes
## configs apply to a single port
## portId is the identifier for the port in `imageconfig` ports.[].id

Comment on lines +148 to +149
## http proxy configs (MUTABLE)
## only "HTTP" protocol ports are supported
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
## http proxy configs (MUTABLE)
## only "HTTP" protocol ports are supported
## http proxy configs (MUTABLE)
## only "HTTP" protocol ports are supported
##

Comment on lines +167 to +169
#set: { "X-RStudio-Root-Path": "{{ .PathPrefix }}" } # for RStudio
#add: {}
#remove: []
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
#set: { "X-RStudio-Root-Path": "{{ .PathPrefix }}" } # for RStudio
#add: {}
#remove: []
#set: { "X-RStudio-Root-Path": "{{ .PathPrefix }}" } # for RStudio
#add: {}
#remove: []

Comment on lines +612 to +617
// validate HTTPProxy is only set if protocol is HTTP
podTemplatePort := podTemplatePortsIdMap[port.Id]
if podTemplatePort.HTTPProxy != nil && podTemplatePort.Protocol != kubefloworgv1beta1.ImagePortProtocolHTTP {
httpProxyPath := imageConfigValuePath.Child("spec", "ports").Key(portId).Child("httpProxy")
errs = append(errs, field.Invalid(httpProxyPath, podTemplatePort.HTTPProxy, "httpProxy can only be set when protocol is HTTP"))
}
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 try to make this so that a lint error will throw here once we add new port protocol enum values (e.g. ssh).

My thinking is to structure it as a switch over protocol, which under the HTTP case that is actually a no-op in this case, so it should just say something like // noop - when adding new protocols, disallow setting unrelated fields

@google-oss-prow google-oss-prow bot removed the lgtm label Sep 25, 2025
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