From a8de501944c6b3d3c8ba6df4a42ff7ad81e581c8 Mon Sep 17 00:00:00 2001 From: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com> Date: Sun, 30 Jun 2024 10:01:43 -0700 Subject: [PATCH] feat(ws): add `containerSecurityContext` to WorkspaceKind (#20) * feat(ws): add `containerSecurityContext` to WorkspaceKind + cleanup descriptions Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com> * feat(ws): make all optional fields go pointers Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com> * feat(ws): remove `readOnlyRootFilesystem=true` from tests Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com> --------- Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com> --- .../controller/api/v1beta1/workspace_types.go | 45 ++- .../api/v1beta1/workspacekind_types.go | 173 ++++++----- .../api/v1beta1/zz_generated.deepcopy.go | 110 ++++++- .../bases/kubeflow.org_workspacekinds.yaml | 292 ++++++++++++++---- .../crd/bases/kubeflow.org_workspaces.yaml | 55 ++-- .../controller/workspace_controller_test.go | 5 +- .../workspacekind_controller_test.go | 43 +-- 7 files changed, 532 insertions(+), 191 deletions(-) diff --git a/workspaces/controller/api/v1beta1/workspace_types.go b/workspaces/controller/api/v1beta1/workspace_types.go index 0c760e81..b61c8abd 100644 --- a/workspaces/controller/api/v1beta1/workspace_types.go +++ b/workspaces/controller/api/v1beta1/workspace_types.go @@ -31,13 +31,12 @@ import ( // WorkspaceSpec defines the desired state of Workspace type WorkspaceSpec struct { - // if the workspace should be paused (no pods running) - //+kubebuilder:default=false + // if the workspace is paused (no pods running) //+kubebuilder:validation:Optional - Paused bool `json:"paused,omitempty"` + //+kubebuilder:default=false + Paused *bool `json:"paused,omitempty"` // the WorkspaceKind to use - //+kubebuilder:validation:Required //+kubebuilder:validation:MinLength:=2 //+kubebuilder:validation:MaxLength:=63 //+kubebuilder:validation:Pattern:=^[a-z0-9][-a-z0-9]*[a-z0-9]$ @@ -52,12 +51,12 @@ type WorkspaceSpec struct { type WorkspacePodTemplate struct { // metadata to be applied to the Pod resource //+kubebuilder:validation:Optional - PodMetadata WorkspacePodMetadata `json:"podMetadata,omitempty"` + PodMetadata *WorkspacePodMetadata `json:"podMetadata,omitempty"` // volume configs Volumes WorkspacePodVolumes `json:"volumes"` - // spawner options, these are the user-selected options from the Workspace Spawner UI which determine the PodSpec of the Workspace Pod + // the selected podTemplate options Options WorkspacePodOptions `json:"options"` } @@ -72,28 +71,33 @@ type WorkspacePodMetadata struct { } type WorkspacePodVolumes struct { - // A PVC to mount as the home directory. - // This PVC must already exist in the Namespace - // This PVC must be RWX (ReadWriteMany, ReadWriteOnce) - // Mount path is defined in the WorkspaceKind under `spec.podTemplate.volumeMounts.home` + // the name of the PVC to mount as the home volume + // - this PVC must already exist in the Namespace + // - this PVC must be RWX (ReadWriteMany, ReadWriteOnce) + // - the mount path is defined in the WorkspaceKind under + // `spec.podTemplate.volumeMounts.home` //+kubebuilder:validation:MinLength:=2 //+kubebuilder:validation:MaxLength:=63 //+kubebuilder:validation:Pattern:=^[a-z0-9][-a-z0-9]*[a-z0-9]$ //+kubebuilder:example="my-home-pvc" Home string `json:"home"` - // additional data PVCs to mount, these PVCs must already exist in the Namespace + // additional PVCs to mount + // - these PVCs must already exist in the Namespace + // - these PVCs must be RWX (ReadWriteMany, ReadWriteOnce) //+kubebuilder:validation:Optional Data []PodVolumeMount `json:"data,omitempty"` } type PodVolumeMount struct { + // the name of the PVC to mount //+kubebuilder:validation:MinLength:=2 //+kubebuilder:validation:MaxLength:=63 //+kubebuilder:validation:Pattern:=^[a-z0-9][-a-z0-9]*[a-z0-9]$ //+kubebuilder:example="my-data-pvc" Name string `json:"name"` + // the mount path for the PVC //+kubebuilder:validation:MinLength:=2 //+kubebuilder:validation:MaxLength:=4096 //+kubebuilder:validation:Pattern:=^/[^/].*$ @@ -102,12 +106,15 @@ type PodVolumeMount struct { } type WorkspacePodOptions struct { - // the id of an image option + // the id of an imageConfig option // - options are defined in WorkspaceKind under // `spec.podTemplate.options.imageConfig.values[]` //+kubebuilder:example="jupyter_scipy_170" ImageConfig string `json:"imageConfig"` + // the id of a podConfig option + // - options are defined in WorkspaceKind under + // `spec.podTemplate.options.podConfig.values[]` //+kubebuilder:example="big_gpu" PodConfig string `json:"podConfig"` } @@ -121,18 +128,22 @@ type WorkspacePodOptions struct { // WorkspaceStatus defines the observed state of Workspace type WorkspaceStatus struct { - // information populated by activity probes, used to determine when to cull + // activity information for the Workspace, used to determine when to cull Activity WorkspaceActivity `json:"activity"` // the time when the Workspace was paused, 0 if the Workspace is not paused //+kubebuilder:example=1704067200 PauseTime int64 `json:"pauseTime"` - // if the current Pod does not reflect the current "desired" state (after redirects) + // if the current Pod does not reflect the current "desired" state + // - true if any `spec.podTemplate.options` have a redirect + // and so will be patched on the next restart + // - true if the WorkspaceKind has changed one of its common `podTemplate` fields + // like `podMetadata`, `probes`, `extraEnv`, or `containerSecurityContext` //+kubebuilder:example=false PendingRestart bool `json:"pendingRestart"` - // actual "target" podTemplateOptions, taking into account redirects + // the `spec.podTemplate.options` which will take effect after the next restart PodTemplateOptions WorkspacePodOptions `json:"podTemplateOptions"` // the current state of the Workspace @@ -140,15 +151,17 @@ type WorkspaceStatus struct { State WorkspaceState `json:"state"` // a human-readable message about the state of the Workspace - // WARNING: this field is NOT FOR MACHINE USE, subject to change without notice + // - WARNING: this field is NOT FOR MACHINE USE, subject to change without notice //+kubebuilder:example="Pod is not ready" StateMessage string `json:"stateMessage"` } type WorkspaceActivity struct { + // the last time activity was observed on the Workspace (UNIX epoch) //+kubebuilder:example=1704067200 LastActivity int64 `json:"lastActivity"` + // the last time we checked for activity on the Workspace (UNIX epoch) //+kubebuilder:example=1704067200 LastUpdate int64 `json:"lastUpdate"` } diff --git a/workspaces/controller/api/v1beta1/workspacekind_types.go b/workspaces/controller/api/v1beta1/workspacekind_types.go index 72d3ba44..964095e8 100644 --- a/workspaces/controller/api/v1beta1/workspacekind_types.go +++ b/workspaces/controller/api/v1beta1/workspacekind_types.go @@ -33,11 +33,9 @@ import ( type WorkspaceKindSpec struct { // spawner config determines how the WorkspaceKind is displayed in the Workspace Spawner UI - //+kubebuilder:validation:Required Spawner WorkspaceKindSpawner `json:"spawner"` // podTemplate is the PodTemplate used to spawn Pods to run Workspaces of this WorkspaceKind - //+kubebuilder:validation:Required PodTemplate WorkspaceKindPodTemplate `json:"podTemplate"` } @@ -55,32 +53,35 @@ type WorkspaceKindSpawner struct { Description string `json:"description"` // if this WorkspaceKind should be hidden from the Workspace Spawner UI + //+kubebuilder:validation:Optional //+kubebuilder:default:=false - Hidden bool `json:"hidden,omitempty"` + Hidden *bool `json:"hidden,omitempty"` // if this WorkspaceKind is deprecated - //+kubebuilder:default:=false //+kubebuilder:validation:Optional - Deprecated bool `json:"deprecated,omitempty"` + //+kubebuilder:default:=false + Deprecated *bool `json:"deprecated,omitempty"` // a message to show in Workspace Spawner UI when the WorkspaceKind is deprecated //+kubebuilder:validation:Optional //+kubebuilder:validation:MinLength:=2 //+kubebuilder:validation:MaxLength:=256 //+kubebuilder:example:="This WorkspaceKind will be removed on 20XX-XX-XX, please use another WorkspaceKind." - DeprecationMessage string `json:"deprecationMessage,omitempty"` + DeprecationMessage *string `json:"deprecationMessage,omitempty"` - // a small (favicon-sized) icon of the WorkspaceKind used in the Workspaces overview table + // the icon of the WorkspaceKind + // - a small (favicon-sized) icon used in the Workspace Spawner UI Icon WorkspaceKindIcon `json:"icon"` - // a 1:1 (card size) logo of the WorkspaceKind used in the Workspace Spawner UI + // the logo of the WorkspaceKind + // - a 1:1 (card size) logo used in the Workspace Spawner UI Logo WorkspaceKindIcon `json:"logo"` } // +kubebuilder:validation:XValidation:message="must specify exactly one of 'url' or 'configMap'",rule="!(has(self.url) && has(self.configMap)) && (has(self.url) || has(self.configMap))" type WorkspaceKindIcon struct { - //+kubebuilder:example="https://jupyter.org/assets/favicons/apple-touch-icon-152x152.png" //+kubebuilder:validation:Optional + //+kubebuilder:example="https://jupyter.org/assets/favicons/apple-touch-icon-152x152.png" Url *string `json:"url,omitempty"` //+kubebuilder:validation:Optional @@ -89,59 +90,69 @@ type WorkspaceKindIcon struct { type WorkspaceKindConfigMap struct { //+kubebuilder:example="my-logos" - //+kubebuilder:validation:Required Name string `json:"name"` //+kubebuilder:example="apple-touch-icon-152x152.png" - //+kubebuilder:validation:Required Key string `json:"key"` } type WorkspaceKindPodTemplate struct { // metadata for Workspace Pods (MUTABLE) - PodMetadata WorkspaceKindPodMetadata `json:"podMetadata,omitempty"` + // - changes are applied the NEXT time each Workspace is PAUSED + //+kubebuilder:validation:Optional + PodMetadata *WorkspaceKindPodMetadata `json:"podMetadata,omitempty"` - // service account configs for Workspace Pods (NOT MUTABLE) - //+kubebuilder:validation:Required + // service account configs for Workspace Pods ServiceAccount WorkspaceKindServiceAccount `json:"serviceAccount"` // culling configs for pausing inactive Workspaces (MUTABLE) - Culling WorkspaceKindCullingConfig `json:"culling,omitempty"` + //+kubebuilder:validation:Optional + Culling *WorkspaceKindCullingConfig `json:"culling,omitempty"` // standard probes to determine Container health (MUTABLE) - // updates to existing Workspaces are applied through the "pending restart" feature - Probes WorkspaceKindProbes `json:"probes,omitempty"` + // - changes are applied the NEXT time each Workspace is PAUSED + //+kubebuilder:validation:Optional + Probes *WorkspaceKindProbes `json:"probes,omitempty"` - // volume mount paths (NOT MUTABLE) - //+kubebuilder:validation:Required + // volume mount paths VolumeMounts WorkspaceKindVolumeMounts `json:"volumeMounts"` // http proxy configs (MUTABLE) - HTTPProxy HTTPProxy `json:"httpProxy,omitempty"` + //+kubebuilder:validation:Optional + HTTPProxy *HTTPProxy `json:"httpProxy,omitempty"` // environment variables for Workspace Pods (MUTABLE) - // updates to existing Workspaces are applied through the "pending restart" feature - // - // The following string templates are available: - // - .PathPrefix: the path prefix of the Workspace (e.g. '/workspace/{profile_name}/{workspace_name}/') + // - changes are applied the NEXT time each Workspace is PAUSED + // - the following string templates are available: + // - `.PathPrefix`: the path prefix of the Workspace (e.g. '/workspace/{profile_name}/{workspace_name}/') + //+kubebuilder:validation:Optional ExtraEnv []v1.EnvVar `json:"extraEnv,omitempty"` + // container SecurityContext for Workspace Pods (MUTABLE) + // - changes are applied the NEXT time each Workspace is PAUSED + //+kubebuilder:validation:Optional + ContainerSecurityContext *v1.SecurityContext `json:"containerSecurityContext,omitempty"` + // options are the user-selectable fields, they determine the PodSpec of the Workspace Options WorkspaceKindPodOptions `json:"options"` } type WorkspaceKindPodMetadata struct { // labels to be applied to the Pod resource + //+kubebuilder:validation:Optional Labels map[string]string `json:"labels,omitempty"` // annotations to be applied to the Pod resource + //+kubebuilder:validation:Optional Annotations map[string]string `json:"annotations,omitempty"` } type WorkspaceKindServiceAccount struct { - // the name of the ServiceAccount; this Service Account MUST already exist in the Namespace of the Workspace, - // the controller will NOT create it. We will not show this WorkspaceKind in the Spawner UI if the SA does not exist in the Namespace. - //+kubebuilder:validation:Required + // the name of the ServiceAccount (NOT MUTABLE) + // - this Service Account MUST already exist in the Namespace + // of the Workspace, the controller will NOT create it + // - we will not show this WorkspaceKind in the Spawner UI + // if the SA does not exist in the Namespace //+kubebuilder:validation:XValidation:rule="self == oldSelf",message="ServiceAccount 'name' is immutable" //+kubebuilder:example="default-editor" Name string `json:"name"` @@ -149,33 +160,38 @@ type WorkspaceKindServiceAccount struct { type WorkspaceKindCullingConfig struct { // if the culling feature is enabled + //+kubebuilder:validation:Optional //+kubebuilder:default=true - Enabled bool `json:"enabled,omitempty"` + Enabled *bool `json:"enabled,omitempty"` // the maximum number of seconds a Workspace can be inactive + //+kubebuilder:validation:Optional //+kubebuilder:validation:Minimum:=60 //+kubebuilder:default=86400 - MaxInactiveSeconds int64 `json:"maxInactiveSeconds,omitempty"` + MaxInactiveSeconds *int64 `json:"maxInactiveSeconds,omitempty"` // the probe used to determine if the Workspace is active - //+kubebuilder:validation:Required ActivityProbe ActivityProbe `json:"activityProbe"` } // +kubebuilder:validation:XValidation:message="must specify exactly one of 'exec' or 'jupyter'",rule="!(has(self.exec) && has(self.jupyter)) && (has(self.exec) || has(self.jupyter))" type ActivityProbe struct { - // a "shell" command to run in the Workspace, if the Workspace had activity in the last 60 seconds, this command - // should return status 0, otherwise it should return status 1 + // a shell command probe + // - if the Workspace had activity in the last 60 seconds this command + // should return status 0, otherwise it should return status 1 + //+kubebuilder:validation:Optional Exec *ActivityProbeExec `json:"exec,omitempty"` - // a Jupyter-specific probe will poll the /api/status endpoint of the Jupyter API, and use the last_activity field - // Users need to be careful that their other probes don't trigger a "last_activity" update, - // e.g. they should only check the health of Jupyter using the /api/status endpoint + // a Jupyter-specific probe + // - will poll the `/api/status` endpoint of the Jupyter API, and use the `last_activity` field + // - note, users need to be careful that their other probes don't trigger a "last_activity" update + // e.g. they should only check the health of Jupyter using the `/api/status` endpoint + //+kubebuilder:validation:Optional Jupyter *ActivityProbeJupyter `json:"jupyter,omitempty"` } type ActivityProbeExec struct { - //+kubebuilder:validation:Required + // the command to run //+kubebuilder:validation:MinItems:=1 //+kubebuilder:example={"bash", "-c", "exit 0"} Command []string `json:"command"` @@ -183,24 +199,27 @@ type ActivityProbeExec struct { // +kubebuilder:validation:XValidation:message="'lastActivity' must be true",rule="has(self.lastActivity) && self.lastActivity" type ActivityProbeJupyter struct { + // if the Jupyter-specific probe is enabled //+kubebuilder:example=true - //+kubebuilder:validation:Required LastActivity bool `json:"lastActivity"` } type WorkspaceKindProbes struct { + // the startup probe for the main container //+kubebuilder:validation:Optional StartupProbe *v1.Probe `json:"startupProbe,omitempty"` + // the liveness probe for the main container //+kubebuilder:validation:Optional LivenessProbe *v1.Probe `json:"livenessProbe,omitempty"` + // the readiness probe for the main container //+kubebuilder:validation:Optional ReadinessProbe *v1.Probe `json:"readinessProbe,omitempty"` } type WorkspaceKindVolumeMounts struct { - //+kubebuilder:validation:Required + // the path to mount the home PVC (NOT MUTABLE) //+kubebuilder:validation:MinLength:=2 //+kubebuilder:validation:MaxLength:=4096 //+kubebuilder:validation:Pattern:=^/[^/].*$ @@ -210,52 +229,55 @@ type WorkspaceKindVolumeMounts struct { } type HTTPProxy struct { - // if the '/workspace/{profile_name}/{workspace_name}/' prefix is to be stripped from incoming HTTP requests - // this only works if the application serves RELATIVE URLs for its assets - //+kubebuilder:default:=false + // if the path prefix is stripped from incoming HTTP requests + // - if true, the '/workspace/{profile_name}/{workspace_name}/' path prefix + // is stripped from incoming requests, the application sees the request + // as if it was made to '/...' + // - this only works if the application serves RELATIVE URLs for its assets //+kubebuilder:validation:Optional - RemovePathPrefix bool `json:"removePathPrefix,omitempty"` + //+kubebuilder:default:=false + RemovePathPrefix *bool `json:"removePathPrefix,omitempty"` // header manipulation rules for incoming HTTP requests - // - // Sets the `spec.http[].headers.request` of the Istio VirtualService: - // https://istio.io/latest/docs/reference/config/networking/virtual-service/#Headers-HeaderOperations - // - // The following string templates are available: - // - .PathPrefix: the path prefix of the Workspace (e.g. '/workspace/{profile_name}/{workspace_name}/') + // - sets the `spec.http[].headers.request` of the Istio VirtualService + // https://istio.io/latest/docs/reference/config/networking/virtual-service/#Headers-HeaderOperations + // - the following string templates are available: + // - `.PathPrefix`: the path prefix of the Workspace (e.g. '/workspace/{profile_name}/{workspace_name}/') //+kubebuilder:validation:Optional - RequestHeaders IstioHeaderOperations `json:"requestHeaders,omitempty"` + RequestHeaders *IstioHeaderOperations `json:"requestHeaders,omitempty"` } type IstioHeaderOperations struct { - // Overwrite the headers specified by key with the given values + // overwrite the headers specified by key with the given values + //+kubebuilder:validation:Optional //+kubebuilder:example:={ "X-RStudio-Root-Path": "{{ .PathPrefix }}" } Set map[string]string `json:"set,omitempty"` - // Append the given values to the headers specified by keys (will create a comma-separated list of values) + // append the given values to the headers specified by keys (will create a comma-separated list of values) + //+kubebuilder:validation:Optional //+kubebuilder:example:={ "My-Header": "value-to-append" } Add map[string]string `json:"add,omitempty"` - // Remove the specified headers + // remove the specified headers + //+kubebuilder:validation:Optional //+kubebuilder:example:={"Header-To-Remove"} Remove []string `json:"remove,omitempty"` } type WorkspaceKindPodOptions struct { - // imageConfig options determine the container image + // imageConfig options ImageConfig ImageConfig `json:"imageConfig"` - // podConfig options determine pod affinity, nodeSelector, tolerations, resources + // podConfig options PodConfig PodConfig `json:"podConfig"` } type ImageConfig struct { - // the id of the default image config for this WorkspaceKind + // the id of the default image config //+kubebuilder:example:="jupyter_scipy_171" Default string `json:"default"` - // the list of image configs that are available to choose from - //+kubebuilder:validation:Required + // the list of image configs that are available //+kubebuilder:validation:MinItems:=1 //+listType:="map" //+listMapKey:="id" @@ -263,20 +285,23 @@ type ImageConfig struct { } type ImageConfigValue struct { + // the id of this image config //+kubebuilder:example:="jupyter_scipy_171" Id string `json:"id"` + // information for the spawner ui Spawner OptionSpawnerInfo `json:"spawner"` + // redirect configs //+kubebuilder:validation:Optional - Redirect OptionRedirect `json:"redirect,omitempty"` + Redirect *OptionRedirect `json:"redirect,omitempty"` + // the spec of the image config Spec ImageConfigSpec `json:"spec"` } type ImageConfigSpec struct { // the container image to use - //+kubebuilder:validation:Required //+kubebuilder:validation:MinLength:=2 //+kubeflow:example="docker.io/kubeflownotebookswg/jupyter-scipy:v1.7.0" Image string `json:"image"` @@ -287,30 +312,27 @@ type ImageConfigSpec struct { //+kubebuilder:validation:Enum:={"Always","IfNotPresent","Never"} ImagePullPolicy *v1.PullPolicy `json:"imagePullPolicy"` - // ports that the container listens on, currently, only HTTP is supported for `protocol` - // if multiple ports are defined, the user will see multiple "Connect" buttons in a dropdown menu on the Workspace overview page - //+kubebuilder:validation:Required + // ports that the container listens on + // - if multiple ports are defined, the user will see multiple "Connect" buttons + // in a dropdown menu on the Workspace overview page //+kubebuilder:validation:MinItems:=1 Ports []ImagePort `json:"ports"` } type ImagePort struct { // the display name of the port - //+kubebuilder:validation:Required //+kubebuilder:validation:MinLength:=2 //+kubebuilder:validation:MaxLength:=64 //+kubebuilder:example:="JupyterLab" DisplayName string `json:"displayName"` // the port number - //+kubebuilder:validation:Required //+kubebuilder:validation:Minimum:=1 //+kubebuilder:validation:Maximum:=65535 //+kubebuilder:example:=8888 Port int32 `json:"port"` // the protocol of the port - //+kubebuilder:validation:Required //+kubebuilder:example:="HTTP" Protocol ImagePortProtocol `json:"protocol"` } @@ -328,7 +350,6 @@ type PodConfig struct { Default string `json:"default"` // the list of pod configs that are available - //+kubebuilder:validation:Required //+kubebuilder:validation:MinItems:=1 //+listType:="map" //+listMapKey:="id" @@ -336,21 +357,25 @@ type PodConfig struct { } type PodConfigValue struct { + // the id of this pod config //+kubebuilder:example="big_gpu" Id string `json:"id"` + // information for the spawner ui Spawner OptionSpawnerInfo `json:"spawner"` + // redirect configs //+kubebuilder:validation:Optional - Redirect OptionRedirect `json:"redirect,omitempty"` + Redirect *OptionRedirect `json:"redirect,omitempty"` + // the spec of the pod config Spec PodConfigSpec `json:"spec"` } type PodConfigSpec struct { // affinity configs for the pod //+kubebuilder:validation:Optional - Affinity v1.Affinity `json:"affinity,omitempty"` + Affinity *v1.Affinity `json:"affinity,omitempty"` // node selector configs for the pod //+kubebuilder:validation:Optional @@ -362,12 +387,11 @@ type PodConfigSpec struct { // resource configs for the "main" container in the pod //+kubebuilder:validation:Optional - Resources v1.ResourceRequirements `json:"resources,omitempty"` + Resources *v1.ResourceRequirements `json:"resources,omitempty"` } type OptionSpawnerInfo struct { // the display name of the option - //+kubebuilder:validation:Required //+kubebuilder:validation:MinLength:=2 //+kubebuilder:validation:MaxLength:=128 DisplayName string `json:"displayName"` @@ -376,7 +400,7 @@ type OptionSpawnerInfo struct { //+kubebuilder:validation:Optional //+kubebuilder:validation:MinLength:=2 //+kubebuilder:validation:MaxLength:=1024 - Description string `json:"description,omitempty"` + Description *string `json:"description,omitempty"` // labels for the option //+kubebuilder:validation:Optional @@ -388,30 +412,31 @@ type OptionSpawnerInfo struct { // if this option should be hidden from the Workspace Spawner UI //+kubebuilder:validation:Optional //+kubebuilder:default:=false - Hidden bool `json:"hidden,omitempty"` + Hidden *bool `json:"hidden,omitempty"` } type OptionSpawnerLabel struct { // the key of the label - //+kubebuilder:validation:Required //+kubebuilder:validation:MinLength:=2 //+kubebuilder:validation:MaxLength:=64 Key string `json:"key"` // the value of the label - //+kubebuilder:validation:Required //+kubebuilder:validation:MinLength:=2 //+kubebuilder:validation:MaxLength:=64 Value string `json:"value"` } type OptionRedirect struct { + // the id of the option to redirect to //+kubebuilder:example:="jupyter_scipy_171" To string `json:"to"` + // if the redirect will be applied after the next restart of the Workspace //+kubebuilder:example:=true WaitForRestart bool `json:"waitForRestart"` + // information about the redirect //+kubebuilder:validation:Optional Message *RedirectMessage `json:"message,omitempty"` } diff --git a/workspaces/controller/api/v1beta1/zz_generated.deepcopy.go b/workspaces/controller/api/v1beta1/zz_generated.deepcopy.go index 4d11b9ff..cfd8a4bf 100644 --- a/workspaces/controller/api/v1beta1/zz_generated.deepcopy.go +++ b/workspaces/controller/api/v1beta1/zz_generated.deepcopy.go @@ -88,7 +88,16 @@ func (in *ActivityProbeJupyter) DeepCopy() *ActivityProbeJupyter { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HTTPProxy) DeepCopyInto(out *HTTPProxy) { *out = *in - in.RequestHeaders.DeepCopyInto(&out.RequestHeaders) + if in.RemovePathPrefix != nil { + in, out := &in.RemovePathPrefix, &out.RemovePathPrefix + *out = new(bool) + **out = **in + } + if in.RequestHeaders != nil { + in, out := &in.RequestHeaders, &out.RequestHeaders + *out = new(IstioHeaderOperations) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPProxy. @@ -152,7 +161,11 @@ func (in *ImageConfigSpec) DeepCopy() *ImageConfigSpec { func (in *ImageConfigValue) DeepCopyInto(out *ImageConfigValue) { *out = *in in.Spawner.DeepCopyInto(&out.Spawner) - in.Redirect.DeepCopyInto(&out.Redirect) + if in.Redirect != nil { + in, out := &in.Redirect, &out.Redirect + *out = new(OptionRedirect) + (*in).DeepCopyInto(*out) + } in.Spec.DeepCopyInto(&out.Spec) } @@ -253,11 +266,21 @@ func (in *OptionRedirect) DeepCopy() *OptionRedirect { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OptionSpawnerInfo) DeepCopyInto(out *OptionSpawnerInfo) { *out = *in + if in.Description != nil { + in, out := &in.Description, &out.Description + *out = new(string) + **out = **in + } if in.Labels != nil { in, out := &in.Labels, &out.Labels *out = make([]OptionSpawnerLabel, len(*in)) copy(*out, *in) } + if in.Hidden != nil { + in, out := &in.Hidden, &out.Hidden + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OptionSpawnerInfo. @@ -310,7 +333,11 @@ func (in *PodConfig) DeepCopy() *PodConfig { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PodConfigSpec) DeepCopyInto(out *PodConfigSpec) { *out = *in - in.Affinity.DeepCopyInto(&out.Affinity) + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(v1.Affinity) + (*in).DeepCopyInto(*out) + } if in.NodeSelector != nil { in, out := &in.NodeSelector, &out.NodeSelector *out = new(v1.NodeSelector) @@ -323,7 +350,11 @@ func (in *PodConfigSpec) DeepCopyInto(out *PodConfigSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - in.Resources.DeepCopyInto(&out.Resources) + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodConfigSpec. @@ -340,7 +371,11 @@ func (in *PodConfigSpec) DeepCopy() *PodConfigSpec { func (in *PodConfigValue) DeepCopyInto(out *PodConfigValue) { *out = *in in.Spawner.DeepCopyInto(&out.Spawner) - in.Redirect.DeepCopyInto(&out.Redirect) + if in.Redirect != nil { + in, out := &in.Redirect, &out.Redirect + *out = new(OptionRedirect) + (*in).DeepCopyInto(*out) + } in.Spec.DeepCopyInto(&out.Spec) } @@ -496,6 +531,16 @@ func (in *WorkspaceKindConfigMap) DeepCopy() *WorkspaceKindConfigMap { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WorkspaceKindCullingConfig) DeepCopyInto(out *WorkspaceKindCullingConfig) { *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.MaxInactiveSeconds != nil { + in, out := &in.MaxInactiveSeconds, &out.MaxInactiveSeconds + *out = new(int64) + **out = **in + } in.ActivityProbe.DeepCopyInto(&out.ActivityProbe) } @@ -615,12 +660,28 @@ func (in *WorkspaceKindPodOptions) DeepCopy() *WorkspaceKindPodOptions { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WorkspaceKindPodTemplate) DeepCopyInto(out *WorkspaceKindPodTemplate) { *out = *in - in.PodMetadata.DeepCopyInto(&out.PodMetadata) + if in.PodMetadata != nil { + in, out := &in.PodMetadata, &out.PodMetadata + *out = new(WorkspaceKindPodMetadata) + (*in).DeepCopyInto(*out) + } out.ServiceAccount = in.ServiceAccount - in.Culling.DeepCopyInto(&out.Culling) - in.Probes.DeepCopyInto(&out.Probes) + if in.Culling != nil { + in, out := &in.Culling, &out.Culling + *out = new(WorkspaceKindCullingConfig) + (*in).DeepCopyInto(*out) + } + if in.Probes != nil { + in, out := &in.Probes, &out.Probes + *out = new(WorkspaceKindProbes) + (*in).DeepCopyInto(*out) + } out.VolumeMounts = in.VolumeMounts - in.HTTPProxy.DeepCopyInto(&out.HTTPProxy) + if in.HTTPProxy != nil { + in, out := &in.HTTPProxy, &out.HTTPProxy + *out = new(HTTPProxy) + (*in).DeepCopyInto(*out) + } if in.ExtraEnv != nil { in, out := &in.ExtraEnv, &out.ExtraEnv *out = make([]v1.EnvVar, len(*in)) @@ -628,6 +689,11 @@ func (in *WorkspaceKindPodTemplate) DeepCopyInto(out *WorkspaceKindPodTemplate) (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.ContainerSecurityContext != nil { + in, out := &in.ContainerSecurityContext, &out.ContainerSecurityContext + *out = new(v1.SecurityContext) + (*in).DeepCopyInto(*out) + } in.Options.DeepCopyInto(&out.Options) } @@ -689,6 +755,21 @@ func (in *WorkspaceKindServiceAccount) DeepCopy() *WorkspaceKindServiceAccount { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WorkspaceKindSpawner) DeepCopyInto(out *WorkspaceKindSpawner) { *out = *in + if in.Hidden != nil { + in, out := &in.Hidden, &out.Hidden + *out = new(bool) + **out = **in + } + if in.Deprecated != nil { + in, out := &in.Deprecated, &out.Deprecated + *out = new(bool) + **out = **in + } + if in.DeprecationMessage != nil { + in, out := &in.DeprecationMessage, &out.DeprecationMessage + *out = new(string) + **out = **in + } in.Icon.DeepCopyInto(&out.Icon) in.Logo.DeepCopyInto(&out.Logo) } @@ -830,7 +911,11 @@ func (in *WorkspacePodOptions) DeepCopy() *WorkspacePodOptions { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WorkspacePodTemplate) DeepCopyInto(out *WorkspacePodTemplate) { *out = *in - in.PodMetadata.DeepCopyInto(&out.PodMetadata) + if in.PodMetadata != nil { + in, out := &in.PodMetadata, &out.PodMetadata + *out = new(WorkspacePodMetadata) + (*in).DeepCopyInto(*out) + } in.Volumes.DeepCopyInto(&out.Volumes) out.Options = in.Options } @@ -868,6 +953,11 @@ func (in *WorkspacePodVolumes) DeepCopy() *WorkspacePodVolumes { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WorkspaceSpec) DeepCopyInto(out *WorkspaceSpec) { *out = *in + if in.Paused != nil { + in, out := &in.Paused, &out.Paused + *out = new(bool) + **out = **in + } in.PodTemplate.DeepCopyInto(&out.PodTemplate) } diff --git a/workspaces/controller/config/crd/bases/kubeflow.org_workspacekinds.yaml b/workspaces/controller/config/crd/bases/kubeflow.org_workspacekinds.yaml index 26f5cea0..e4b2b238 100644 --- a/workspaces/controller/config/crd/bases/kubeflow.org_workspacekinds.yaml +++ b/workspaces/controller/config/crd/bases/kubeflow.org_workspacekinds.yaml @@ -56,6 +56,175 @@ spec: description: podTemplate is the PodTemplate used to spawn Pods to run Workspaces of this WorkspaceKind properties: + containerSecurityContext: + description: |- + container SecurityContext for Workspace Pods (MUTABLE) + - changes are applied the NEXT time each Workspace is PAUSED + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object culling: description: culling configs for pausing inactive Workspaces (MUTABLE) properties: @@ -65,10 +234,12 @@ spec: properties: exec: description: |- - a "shell" command to run in the Workspace, if the Workspace had activity in the last 60 seconds, this command - should return status 0, otherwise it should return status 1 + a shell command probe + - if the Workspace had activity in the last 60 seconds this command + should return status 0, otherwise it should return status 1 properties: command: + description: the command to run example: - bash - -c @@ -81,14 +252,14 @@ spec: - command type: object jupyter: - description: "a Jupyter-specific probe will poll the /api/status - endpoint of the Jupyter API, and use the last_activity - field\n Users need to be careful that their other probes - don't trigger a \"last_activity\" update,\n\te.g. they - should only check the health of Jupyter using the /api/status - endpoint" + description: |- + a Jupyter-specific probe + - will poll the `/api/status` endpoint of the Jupyter API, and use the `last_activity` field + - note, users need to be careful that their other probes don't trigger a "last_activity" update + e.g. they should only check the health of Jupyter using the `/api/status` endpoint properties: lastActivity: + description: if the Jupyter-specific probe is enabled example: true type: boolean required: @@ -119,11 +290,9 @@ spec: extraEnv: description: |- environment variables for Workspace Pods (MUTABLE) - updates to existing Workspaces are applied through the "pending restart" feature - - - The following string templates are available: - - .PathPrefix: the path prefix of the Workspace (e.g. '/workspace/{profile_name}/{workspace_name}/') + - changes are applied the NEXT time each Workspace is PAUSED + - the following string templates are available: + - `.PathPrefix`: the path prefix of the Workspace (e.g. '/workspace/{profile_name}/{workspace_name}/') items: description: EnvVar represents an environment variable present in a Container. @@ -242,31 +411,30 @@ spec: removePathPrefix: default: false description: |- - if the '/workspace/{profile_name}/{workspace_name}/' prefix is to be stripped from incoming HTTP requests - this only works if the application serves RELATIVE URLs for its assets + if the path prefix is stripped from incoming HTTP requests + - if true, the '/workspace/{profile_name}/{workspace_name}/' path prefix + is stripped from incoming requests, the application sees the request + as if it was made to '/...' + - this only works if the application serves RELATIVE URLs for its assets type: boolean requestHeaders: description: |- header manipulation rules for incoming HTTP requests - - - Sets the `spec.http[].headers.request` of the Istio VirtualService: - https://istio.io/latest/docs/reference/config/networking/virtual-service/#Headers-HeaderOperations - - - The following string templates are available: - - .PathPrefix: the path prefix of the Workspace (e.g. '/workspace/{profile_name}/{workspace_name}/') + - sets the `spec.http[].headers.request` of the Istio VirtualService + https://istio.io/latest/docs/reference/config/networking/virtual-service/#Headers-HeaderOperations + - the following string templates are available: + - `.PathPrefix`: the path prefix of the Workspace (e.g. '/workspace/{profile_name}/{workspace_name}/') properties: add: additionalProperties: type: string - description: Append the given values to the headers specified + description: append the given values to the headers specified by keys (will create a comma-separated list of values) example: My-Header: value-to-append type: object remove: - description: Remove the specified headers + description: remove the specified headers example: - Header-To-Remove items: @@ -275,7 +443,7 @@ spec: set: additionalProperties: type: string - description: Overwrite the headers specified by key with + description: overwrite the headers specified by key with the given values example: X-RStudio-Root-Path: '{{ .PathPrefix }}' @@ -287,24 +455,25 @@ spec: the PodSpec of the Workspace properties: imageConfig: - description: imageConfig options determine the container image + description: imageConfig options properties: default: - description: the id of the default image config for this - WorkspaceKind + description: the id of the default image config example: jupyter_scipy_171 type: string values: description: the list of image configs that are available - to choose from items: properties: id: + description: the id of this image config example: jupyter_scipy_171 type: string redirect: + description: redirect configs properties: message: + description: information about the redirect properties: level: description: the importance level of the @@ -328,9 +497,13 @@ spec: - text type: object to: + description: the id of the option to redirect + to example: jupyter_scipy_171 type: string waitForRestart: + description: if the redirect will be applied + after the next restart of the Workspace example: true type: boolean required: @@ -338,6 +511,7 @@ spec: - waitForRestart type: object spawner: + description: information for the spawner ui properties: description: description: a description of the option @@ -381,6 +555,7 @@ spec: - displayName type: object spec: + description: the spec of the image config properties: image: description: the container image to use @@ -397,8 +572,9 @@ spec: type: string ports: description: |- - ports that the container listens on, currently, only HTTP is supported for `protocol` - if multiple ports are defined, the user will see multiple "Connect" buttons in a dropdown menu on the Workspace overview page + ports that the container listens on + - if multiple ports are defined, the user will see multiple "Connect" buttons + in a dropdown menu on the Workspace overview page items: properties: displayName: @@ -446,8 +622,7 @@ spec: - values type: object podConfig: - description: podConfig options determine pod affinity, nodeSelector, - tolerations, resources + description: podConfig options properties: default: description: the id of the default pod config @@ -458,11 +633,14 @@ spec: items: properties: id: + description: the id of this pod config example: big_gpu type: string redirect: + description: redirect configs properties: message: + description: information about the redirect properties: level: description: the importance level of the @@ -486,9 +664,13 @@ spec: - text type: object to: + description: the id of the option to redirect + to example: jupyter_scipy_171 type: string waitForRestart: + description: if the redirect will be applied + after the next restart of the Workspace example: true type: boolean required: @@ -496,6 +678,7 @@ spec: - waitForRestart type: object spawner: + description: information for the spawner ui properties: description: description: a description of the option @@ -539,6 +722,7 @@ spec: - displayName type: object spec: + description: the spec of the pod config properties: affinity: description: affinity configs for the pod @@ -1673,7 +1857,9 @@ spec: - podConfig type: object podMetadata: - description: metadata for Workspace Pods (MUTABLE) + description: |- + metadata for Workspace Pods (MUTABLE) + - changes are applied the NEXT time each Workspace is PAUSED properties: annotations: additionalProperties: @@ -1689,12 +1875,10 @@ spec: probes: description: |- standard probes to determine Container health (MUTABLE) - updates to existing Workspaces are applied through the "pending restart" feature + - changes are applied the NEXT time each Workspace is PAUSED properties: livenessProbe: - description: |- - Probe describes a health check to be performed against a container to determine whether it is - alive or ready to receive traffic. + description: the liveness probe for the main container properties: exec: description: Exec specifies the action to take. @@ -1845,9 +2029,7 @@ spec: type: integer type: object readinessProbe: - description: |- - Probe describes a health check to be performed against a container to determine whether it is - alive or ready to receive traffic. + description: the readiness probe for the main container properties: exec: description: Exec specifies the action to take. @@ -1998,9 +2180,7 @@ spec: type: integer type: object startupProbe: - description: |- - Probe describes a health check to be performed against a container to determine whether it is - alive or ready to receive traffic. + description: the startup probe for the main container properties: exec: description: Exec specifies the action to take. @@ -2152,12 +2332,15 @@ spec: type: object type: object serviceAccount: - description: service account configs for Workspace Pods (NOT MUTABLE) + description: service account configs for Workspace Pods properties: name: description: |- - the name of the ServiceAccount; this Service Account MUST already exist in the Namespace of the Workspace, - the controller will NOT create it. We will not show this WorkspaceKind in the Spawner UI if the SA does not exist in the Namespace. + the name of the ServiceAccount (NOT MUTABLE) + - this Service Account MUST already exist in the Namespace + of the Workspace, the controller will NOT create it + - we will not show this WorkspaceKind in the Spawner UI + if the SA does not exist in the Namespace example: default-editor type: string x-kubernetes-validations: @@ -2167,9 +2350,10 @@ spec: - name type: object volumeMounts: - description: volume mount paths (NOT MUTABLE) + description: volume mount paths properties: home: + description: the path to mount the home PVC (NOT MUTABLE) example: /home/jovyan maxLength: 4096 minLength: 2 @@ -2220,8 +2404,9 @@ spec: Spawner UI type: boolean icon: - description: a small (favicon-sized) icon of the WorkspaceKind - used in the Workspaces overview table + description: |- + the icon of the WorkspaceKind + - a small (favicon-sized) icon used in the Workspace Spawner UI properties: configMap: properties: @@ -2244,8 +2429,9 @@ spec: rule: '!(has(self.url) && has(self.configMap)) && (has(self.url) || has(self.configMap))' logo: - description: a 1:1 (card size) logo of the WorkspaceKind used - in the Workspace Spawner UI + description: |- + the logo of the WorkspaceKind + - a 1:1 (card size) logo used in the Workspace Spawner UI properties: configMap: properties: diff --git a/workspaces/controller/config/crd/bases/kubeflow.org_workspaces.yaml b/workspaces/controller/config/crd/bases/kubeflow.org_workspaces.yaml index 2726be41..593bf6fb 100644 --- a/workspaces/controller/config/crd/bases/kubeflow.org_workspaces.yaml +++ b/workspaces/controller/config/crd/bases/kubeflow.org_workspaces.yaml @@ -56,24 +56,26 @@ spec: rule: self == oldSelf paused: default: false - description: if the workspace should be paused (no pods running) + description: if the workspace is paused (no pods running) type: boolean podTemplate: description: options for "podTemplate"-type WorkspaceKinds properties: options: - description: spawner options, these are the user-selected options - from the Workspace Spawner UI which determine the PodSpec of - the Workspace Pod + description: the selected podTemplate options properties: imageConfig: description: |- - the id of an image option + the id of an imageConfig option - options are defined in WorkspaceKind under `spec.podTemplate.options.imageConfig.values[]` example: jupyter_scipy_170 type: string podConfig: + description: |- + the id of a podConfig option + - options are defined in WorkspaceKind under + `spec.podTemplate.options.podConfig.values[]` example: big_gpu type: string required: @@ -98,17 +100,21 @@ spec: description: volume configs properties: data: - description: additional data PVCs to mount, these PVCs must - already exist in the Namespace + description: |- + additional PVCs to mount + - these PVCs must already exist in the Namespace + - these PVCs must be RWX (ReadWriteMany, ReadWriteOnce) items: properties: mountPath: + description: the mount path for the PVC example: /data/my-data maxLength: 4096 minLength: 2 pattern: ^/[^/].*$ type: string name: + description: the name of the PVC to mount example: my-data-pvc maxLength: 63 minLength: 2 @@ -121,10 +127,11 @@ spec: type: array home: description: |- - A PVC to mount as the home directory. - This PVC must already exist in the Namespace - This PVC must be RWX (ReadWriteMany, ReadWriteOnce) - Mount path is defined in the WorkspaceKind under `spec.podTemplate.volumeMounts.home` + the name of the PVC to mount as the home volume + - this PVC must already exist in the Namespace + - this PVC must be RWX (ReadWriteMany, ReadWriteOnce) + - the mount path is defined in the WorkspaceKind under + `spec.podTemplate.volumeMounts.home` example: my-home-pvc maxLength: 63 minLength: 2 @@ -145,14 +152,18 @@ spec: description: WorkspaceStatus defines the observed state of Workspace properties: activity: - description: information populated by activity probes, used to determine + description: activity information for the Workspace, used to determine when to cull properties: lastActivity: + description: the last time activity was observed on the Workspace + (UNIX epoch) example: 1704067200 format: int64 type: integer lastUpdate: + description: the last time we checked for activity on the Workspace + (UNIX epoch) example: 1704067200 format: int64 type: integer @@ -167,22 +178,30 @@ spec: format: int64 type: integer pendingRestart: - description: if the current Pod does not reflect the current "desired" - state (after redirects) + description: |- + if the current Pod does not reflect the current "desired" state + - true if any `spec.podTemplate.options` have a redirect + and so will be patched on the next restart + - true if the WorkspaceKind has changed one of its common `podTemplate` fields + like `podMetadata`, `probes`, `extraEnv`, or `containerSecurityContext` example: false type: boolean podTemplateOptions: - description: actual "target" podTemplateOptions, taking into account - redirects + description: the `spec.podTemplate.options` which will take effect + after the next restart properties: imageConfig: description: |- - the id of an image option + the id of an imageConfig option - options are defined in WorkspaceKind under `spec.podTemplate.options.imageConfig.values[]` example: jupyter_scipy_170 type: string podConfig: + description: |- + the id of a podConfig option + - options are defined in WorkspaceKind under + `spec.podTemplate.options.podConfig.values[]` example: big_gpu type: string required: @@ -203,7 +222,7 @@ spec: stateMessage: description: |- a human-readable message about the state of the Workspace - WARNING: this field is NOT FOR MACHINE USE, subject to change without notice + - WARNING: this field is NOT FOR MACHINE USE, subject to change without notice example: Pod is not ready type: string required: diff --git a/workspaces/controller/internal/controller/workspace_controller_test.go b/workspaces/controller/internal/controller/workspace_controller_test.go index c1a86040..420bd057 100644 --- a/workspaces/controller/internal/controller/workspace_controller_test.go +++ b/workspaces/controller/internal/controller/workspace_controller_test.go @@ -18,6 +18,7 @@ package controller import ( "context" + "k8s.io/utils/ptr" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -52,10 +53,10 @@ var _ = Describe("Workspace Controller", func() { Namespace: "default", }, Spec: kubefloworgv1beta1.WorkspaceSpec{ - Paused: false, + Paused: ptr.To(false), Kind: "juptyer-lab", PodTemplate: kubefloworgv1beta1.WorkspacePodTemplate{ - PodMetadata: kubefloworgv1beta1.WorkspacePodMetadata{ + PodMetadata: &kubefloworgv1beta1.WorkspacePodMetadata{ Labels: nil, Annotations: nil, }, diff --git a/workspaces/controller/internal/controller/workspacekind_controller_test.go b/workspaces/controller/internal/controller/workspacekind_controller_test.go index 6124bcf3..45052675 100644 --- a/workspaces/controller/internal/controller/workspacekind_controller_test.go +++ b/workspaces/controller/internal/controller/workspacekind_controller_test.go @@ -57,9 +57,9 @@ var _ = Describe("WorkspaceKind Controller", func() { Spawner: kubefloworgv1beta1.WorkspaceKindSpawner{ DisplayName: "JupyterLab Notebook", Description: "A Workspace which runs JupyterLab in a Pod", - Hidden: false, - Deprecated: false, - DeprecationMessage: "This WorkspaceKind will be removed on 20XX-XX-XX, please use another WorkspaceKind.", + Hidden: ptr.To(false), + Deprecated: ptr.To(false), + DeprecationMessage: ptr.To("This WorkspaceKind will be removed on 20XX-XX-XX, please use another WorkspaceKind."), Icon: kubefloworgv1beta1.WorkspaceKindIcon{ Url: ptr.To("https://jupyter.org/assets/favicons/apple-touch-icon-152x152.png"), }, @@ -72,26 +72,26 @@ var _ = Describe("WorkspaceKind Controller", func() { }, PodTemplate: kubefloworgv1beta1.WorkspaceKindPodTemplate{ - PodMetadata: kubefloworgv1beta1.WorkspaceKindPodMetadata{}, + PodMetadata: &kubefloworgv1beta1.WorkspaceKindPodMetadata{}, ServiceAccount: kubefloworgv1beta1.WorkspaceKindServiceAccount{ Name: "default-editor", }, - Culling: kubefloworgv1beta1.WorkspaceKindCullingConfig{ - Enabled: true, - MaxInactiveSeconds: 86400, + Culling: &kubefloworgv1beta1.WorkspaceKindCullingConfig{ + Enabled: ptr.To(true), + MaxInactiveSeconds: ptr.To(int64(86400)), ActivityProbe: kubefloworgv1beta1.ActivityProbe{ Exec: &kubefloworgv1beta1.ActivityProbeExec{ Command: []string{"bash", "-c", "exit 0"}, }, }, }, - Probes: kubefloworgv1beta1.WorkspaceKindProbes{}, + Probes: &kubefloworgv1beta1.WorkspaceKindProbes{}, VolumeMounts: kubefloworgv1beta1.WorkspaceKindVolumeMounts{ Home: "/home/jovyan", }, - HTTPProxy: kubefloworgv1beta1.HTTPProxy{ - RemovePathPrefix: false, - RequestHeaders: kubefloworgv1beta1.IstioHeaderOperations{ + HTTPProxy: &kubefloworgv1beta1.HTTPProxy{ + RemovePathPrefix: ptr.To(false), + RequestHeaders: &kubefloworgv1beta1.IstioHeaderOperations{ Set: map[string]string{"X-RStudio-Root-Path": "{{ .PathPrefix }}"}, Add: map[string]string{}, Remove: []string{}, @@ -103,6 +103,13 @@ var _ = Describe("WorkspaceKind Controller", func() { Value: "{{ .PathPrefix }}", }, }, + ContainerSecurityContext: &v1.SecurityContext{ + AllowPrivilegeEscalation: ptr.To(false), + Capabilities: &v1.Capabilities{ + Drop: []v1.Capability{"ALL"}, + }, + RunAsNonRoot: ptr.To(true), + }, Options: kubefloworgv1beta1.WorkspaceKindPodOptions{ ImageConfig: kubefloworgv1beta1.ImageConfig{ Default: "jupyter_scipy_171", @@ -111,10 +118,10 @@ var _ = Describe("WorkspaceKind Controller", func() { Id: "jupyter_scipy_170", Spawner: kubefloworgv1beta1.OptionSpawnerInfo{ DisplayName: "jupyter-scipy:v1.7.0", - Description: "JupyterLab 1.7.0, with SciPy Packages", - Hidden: true, + Description: ptr.To("JupyterLab 1.7.0, with SciPy Packages"), + Hidden: ptr.To(true), }, - Redirect: kubefloworgv1beta1.OptionRedirect{ + Redirect: &kubefloworgv1beta1.OptionRedirect{ To: "jupyter_scipy_171", WaitForRestart: true, Message: &kubefloworgv1beta1.RedirectMessage{ @@ -142,12 +149,12 @@ var _ = Describe("WorkspaceKind Controller", func() { Id: "small_cpu", Spawner: kubefloworgv1beta1.OptionSpawnerInfo{ DisplayName: "Small CPU", - Description: "Pod with 1 CPU, 2 GB RAM, and 1 GPU", - Hidden: false, + Description: ptr.To("Pod with 1 CPU, 2 GB RAM, and 1 GPU"), + Hidden: ptr.To(false), }, - Redirect: kubefloworgv1beta1.OptionRedirect{}, + Redirect: nil, Spec: kubefloworgv1beta1.PodConfigSpec{ - Resources: v1.ResourceRequirements{ + Resources: &v1.ResourceRequirements{ Requests: map[v1.ResourceName]resource.Quantity{ "cpu": resource.MustParse("1"), "memory": resource.MustParse("2Gi"),