Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
e07cc59
Tilt setup for backend
Jun 15, 2025
0ac6f9f
feat(ws): added namespaces tab to workspace kind details (#406)
paulovmr Jun 5, 2025
92661f0
chore(ws): Upgrade vulnerable package webpack-dev-server (#407)
paulovmr Jun 5, 2025
4ce7875
fix(ws): Action Button Alignment and Jupyter Image Display (#408)
jenny-s51 Jun 16, 2025
cd02eb4
fix(ws): Expose active nav item on initial Workspaces page load (#419)
jenny-s51 Jun 16, 2025
99538a7
chore(ws): enforce named imports for react hooks (#414)
paulovmr Jun 16, 2025
72b3ba9
chore(ws): Upgrade vulnerable packages (#427)
paulovmr Jun 16, 2025
ca7f656
feat(ws): add `WorkspaceKindSummary` page and other improvements arou…
caponetto Jun 17, 2025
0c87cb6
chore(ws): allowed theme configuration during frontend start (#438)
paulovmr Jun 24, 2025
234dee0
feat(ws): Notebooks v2 Create Workspace Kind (#365)
thaorell Jun 25, 2025
c0b8f7b
chore(ws): lint frontend on each commit (#440)
caponetto Jun 25, 2025
eb8d3ac
chore(ws): show ESLint errors from local rules on IDE (#439)
caponetto Jun 25, 2025
4c91627
feat(ws): validate podMetadata for ws and wsk in webhook (#436)
thesuperzapper Jun 26, 2025
eb70a94
feat(ws): fix swagger warnings and only generate json output (#424)
liavweiss Jun 26, 2025
48b690e
ci(ws): archive frontend cypress test results in github actions (#396)
jiridanek Jun 26, 2025
e3978c2
chore: reference ghcr.io images in workspacekind yaml (#305)
andyatmiami Jun 26, 2025
b6e664c
chore: add OWNERS files with reviewers and labels (#450)
thesuperzapper Jun 26, 2025
eae9e23
fix(ws): backend dockerfile (#386)
liavweiss Jun 26, 2025
a4cd1c2
feat(ws): add fallback mechanism to broken images (#448)
caponetto Jun 27, 2025
6ba18c0
feat: refactor Form View to Edit only (#451)
thaorell Jun 27, 2025
23fed9c
feat(ws): Make Create Workspace Kind button visible (#466)
thaorell Jul 2, 2025
c6e81c2
fix(ws): Improve Workspace Creation Wizard Step Descriptions (#452)
jenny-s51 Jul 4, 2025
d680ea0
feat: workspace kind Edit Pod Configs (#425)
thaorell Jul 4, 2025
db3e000
fix: removed blank space on left of dropdown options (#329)
dominikkawka Jul 4, 2025
60d6de0
feat(ws): backend api to create wsk with YAML (#434)
asaadbalum Jul 6, 2025
08c206d
feat(ws): prepare frontend for validation errors during WorkspaceKind…
caponetto Jul 7, 2025
989fe53
chore(ws): added cspell to enforce spelling check (#469)
paulovmr Jul 7, 2025
da615f5
chore(ws): added prettier to test and test:fix scripts (#470)
paulovmr Jul 7, 2025
9607fab
fix(ws): Updates to Table Columns, Expandable Rows, and Theming (#432)
jenny-s51 Jul 7, 2025
3fed049
chore(ws): upgrade deprecated rimraf transitive dependency (#474)
paulovmr Jul 9, 2025
3feccf7
fix(ws): Improve workspace form drawer details and wizard flow (#467)
jenny-s51 Jul 9, 2025
5e9c88f
chore(ws): Add support for PF utility classes (#476)
jenny-s51 Jul 10, 2025
0e90e5d
feat(ws): Add advanced pod configurations in Workspace Edit (#468)
thaorell Jul 10, 2025
3e7f44a
feat(ws): Refactor restYAML to restFILE (#478)
thaorell Jul 17, 2025
13a66ae
fix(ws): normalize text case for workspace count buttons, update colu…
jenny-s51 Jul 17, 2025
de0e5c4
feat(ws): Make Workspace Kind drawer resizable and add table view to …
thaorell Jul 18, 2025
3218768
fix(ws): Implement dual scrolling for workspace kind wizard (#484)
jenny-s51 Jul 21, 2025
296f63f
chore(ws): enforce component specific imports (#475)
paulovmr Jul 21, 2025
b21cf69
chore(ws): Upgrade vulnerable packages (#495)
paulovmr Jul 23, 2025
b18812a
fix(ws): Apply sentence case to text elements across UI (#497)
jenny-s51 Jul 24, 2025
4b88c15
improve: UX Enhancements in workspace summary (#473)
dominikkawka Jul 24, 2025
f1f1c83
feat(ws): add ws counts to backend wsk model (#368)
roee1313 Jul 24, 2025
dd94a8f
feat(ws): containerize frontend component (#394)
noalimoy Jul 24, 2025
f02f07c
feat(ws): add workspace pause actions backend API (#340)
andyatmiami Jul 24, 2025
bdbfe1b
fix(ws): update frontend to support latest start/stop API changes (#503)
caponetto Jul 25, 2025
7bed0be
fix(ws): Refactors toolbar and filter logic to fix "clear all filters…
jenny-s51 Jul 29, 2025
7a6bb30
feat(ws): fix workspaces table pagination (#506)
paulovmr Jul 29, 2025
77c69c5
feat(ws): use workspace counts from API response (#508)
caponetto Jul 29, 2025
039e0c9
feat(ws): add @ID swag annotation to handlers (#488)
andyatmiami Jul 31, 2025
c50bdbb
chore(ws): update swag to 1.16.6 for required fields (#489)
andyatmiami Jul 31, 2025
2d5b830
chore(ws): comment workspace details logs and pod template tabs while…
paulovmr Aug 5, 2025
dcf6b93
feat(ws): automate generation of types and HTTP client layer from Swa…
caponetto Aug 5, 2025
673989f
feat: Make Frontend Basepath Configurable via APP_PREFIX env variable…
thaorell Aug 5, 2025
053f278
feat: Conditionally render masthead on Production and Standalone mode…
thaorell Aug 5, 2025
d2b97e7
ci(ws): run client generator on frontend PR check (#519)
caponetto Aug 7, 2025
fd2dc79
feat: Refactor APP_PREFIX to const.ts (#523)
thaorell Aug 7, 2025
a932c0f
fix(ws): fixed filter by labels during workspace creation (#524)
paulovmr Aug 7, 2025
1b14efd
test: add unit tests for frontend hooks (#527)
caponetto Aug 12, 2025
1f5c6e1
chore: Upgrade PatternFly to 6.3.0 (#532)
jenny-s51 Aug 20, 2025
1950ea3
fix: fixed workspace kind summary breadcrumb navigation (#535)
paulovmr Aug 20, 2025
877e6de
feat(ws): add manifests for backend (#455)
liavweiss Aug 21, 2025
42ffd9b
feat(ws): add manifests for frontend (#487)
noalimoy Aug 21, 2025
e666e2e
feat: add environment configuration files for frontend (#536)
caponetto Aug 26, 2025
b210a56
feat: enhance husky pre-commit hook to conditionally run lint checks …
caponetto Aug 27, 2025
95431b4
feat(ws): frontend Makefile to support deploy (#534)
mkoushni Sep 4, 2025
253b25e
feat: integrate the frontend shared libraries (#552)
caponetto Sep 4, 2025
f3a4b51
feat: Add dry-run functionality for WorkspaceKind creation
Sep 14, 2025
073b16f
Merge branch 'notebooks-v2' into notebooks-v2
bhaktinarvekar Sep 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions workspaces/backend/Tiltfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
load("ext://restart_process", "docker_build_with_restart")

local_resource(
"backend-bin",
"CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ../bin/backend cmd/main.go",
deps=["cmd", "internal", "go.mod", "go.sum"],
)


docker_build_with_restart(
"backend",
context="..", # this is where dev.Dockerfile lives
dockerfile="devenv/dev.Dockerfile",
entrypoint=["/backend"],
only=["bin/backend"],
live_update=[
sync("../bin/backend", "/backend"),
],
)

k8s_yaml("devenv/backend.yaml")

k8s_resource("backend", port_forwards=4000)
23 changes: 21 additions & 2 deletions workspaces/backend/api/workspacekinds_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"io"
"net/http"
"strings"

"github.com/julienschmidt/httprouter"
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
Expand Down Expand Up @@ -153,9 +154,17 @@ func (a *App) GetWorkspaceKindsHandler(w http.ResponseWriter, r *http.Request, _
// @Failure 500 {object} ErrorEnvelope "Internal server error. An unexpected error occurred on the server."
// @Router /workspacekinds [post]
func (a *App) CreateWorkspaceKindHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
// Parse dry-run query parameter
dryRun := r.URL.Query().Get("dry_run")
if dryRun != "" && dryRun != "true" && dryRun != "false" {
a.badRequestResponse(w, r, fmt.Errorf("Invalid dry_run value. Must be 'true' or 'false'"))
return
}
isDryRun := dryRun == "true"

// validate the Content-Type header
if success := a.ValidateContentType(w, r, MediaTypeYaml); !success {
if !strings.EqualFold(r.Header.Get("Content-Type"), MediaTypeYaml) {
a.unsupportedMediaTypeResponse(w, r, fmt.Errorf("Only application/yaml is supported"))
return
}

Expand Down Expand Up @@ -204,7 +213,7 @@ func (a *App) CreateWorkspaceKindHandler(w http.ResponseWriter, r *http.Request,
}
// ============================================================

createdWorkspaceKind, err := a.repositories.WorkspaceKind.Create(r.Context(), workspaceKind)
createdWorkspaceKind, err := a.repositories.WorkspaceKind.Create(r.Context(), workspaceKind, isDryRun)
if err != nil {
if errors.Is(err, repository.ErrWorkspaceKindAlreadyExists) {
a.conflictResponse(w, r, err)
Expand All @@ -219,6 +228,16 @@ func (a *App) CreateWorkspaceKindHandler(w http.ResponseWriter, r *http.Request,
return
}

// Set response Content-Type header
w.Header().Set("Content-Type", "application/json")

// Return appropriate response based on dry-run
if isDryRun {
responseEnvelope := &WorkspaceKindEnvelope{Data: *createdWorkspaceKind}
a.dataResponse(w, r, responseEnvelope)
return
}

// calculate the GET location for the created workspace kind (for the Location header)
location := a.LocationGetWorkspaceKind(createdWorkspaceKind.Name)

Expand Down
201 changes: 201 additions & 0 deletions workspaces/backend/api/workspacekinds_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation/field"
Expand Down Expand Up @@ -507,4 +508,204 @@ metadata:
))
})
})

// NOTE: these tests create and delete resources on the cluster, so cannot be run in parallel.
// therefore, we run them using the `Serial` Ginkgo decorator.
Context("when creating a WorkspaceKind", Serial, func() {

var newWorkspaceKindName = "wsk-create-test"
var validYAML []byte

BeforeEach(func() {
validYAML = []byte(fmt.Sprintf(`
apiVersion: kubeflow.org/v1beta1
kind: WorkspaceKind
metadata:
name: %s
spec:
spawner:
displayName: "JupyterLab Notebook"
description: "A Workspace which runs JupyterLab in a Pod"
icon:
url: "https://jupyter.org/assets/favicons/apple-touch-icon.png"
logo:
url: "https://jupyter.org/assets/logos/jupyter/jupyter.png"
podTemplate:
serviceAccount:
name: default-editor
volumeMounts:
home: "/home/jovyan"
options:
imageConfig:
default: "jupyterlab_scipy_180"
values:
- id: "jupyterlab_scipy_180"
displayName: "JupyterLab SciPy 1.8.0"
description: "JupyterLab with SciPy 1.8.0"
spec:
image: "jupyter/scipy-notebook:2024.1.0"
podConfig:
default: "tiny_cpu"
values:
- id: "tiny_cpu"
displayName: "Tiny CPU"
description: "1 CPU core, 2GB RAM"
spec:
resources:
requests:
cpu: "100m"
memory: "512Mi"
limits:
cpu: "1"
memory: "2Gi"
`, newWorkspaceKindName))
})

AfterEach(func() {
By("deleting the WorkspaceKind if it exists")
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{
ObjectMeta: metav1.ObjectMeta{
Name: newWorkspaceKindName,
},
}
_ = k8sClient.Delete(ctx, workspaceKind)
})

It("should create a WorkspaceKind successfully without dry-run", func() {
By("creating the HTTP request")
req, err := http.NewRequest(http.MethodPost, AllWorkspaceKindsPath, bytes.NewReader(validYAML))
Expect(err).NotTo(HaveOccurred())

By("setting required headers")
req.Header.Set("Content-Type", MediaTypeYaml)
req.Header.Set(userIdHeader, adminUser)

By("executing CreateWorkspaceKindHandler")
ps := httprouter.Params{}
rr := httptest.NewRecorder()
a.CreateWorkspaceKindHandler(rr, req, ps)
rs := rr.Result()
defer rs.Body.Close()

By("verifying the HTTP response status code")
Expect(rs.StatusCode).To(Equal(http.StatusCreated), descUnexpectedHTTPStatus, rr.Body.String())

By("verifying the Location header")
location := rs.Header.Get("Location")
Expect(location).To(Equal(fmt.Sprintf("/api/v1/workspacekinds/%s", newWorkspaceKindName)))

By("reading and parsing the response body")
body, err := io.ReadAll(rs.Body)
Expect(err).NotTo(HaveOccurred())

var response WorkspaceKindCreateEnvelope
err = json.Unmarshal(body, &response)
Expect(err).NotTo(HaveOccurred())

By("verifying the created resource exists in the cluster")
createdWorkspaceKind := &kubefloworgv1beta1.WorkspaceKind{}
err = k8sClient.Get(ctx, types.NamespacedName{Name: newWorkspaceKindName}, createdWorkspaceKind)
Expect(err).NotTo(HaveOccurred())

By("verifying the response matches the created resource")
expectedWorkspaceKind := models.NewWorkspaceKindModelFromWorkspaceKind(createdWorkspaceKind)
Expect(response.Data).To(BeComparableTo(expectedWorkspaceKind))
})

It("should validate WorkspaceKind with dry-run=true without creating it", func() {
By("creating the HTTP request with dry-run=true")
req, err := http.NewRequest(http.MethodPost, AllWorkspaceKindsPath+"?dry_run=true", bytes.NewReader(validYAML))
Expect(err).NotTo(HaveOccurred())

By("setting required headers")
req.Header.Set("Content-Type", MediaTypeYaml)
req.Header.Set(userIdHeader, adminUser)

By("executing CreateWorkspaceKindHandler")
ps := httprouter.Params{}
rr := httptest.NewRecorder()
a.CreateWorkspaceKindHandler(rr, req, ps)
rs := rr.Result()
defer rs.Body.Close()

By("verifying the HTTP response status code")
Expect(rs.StatusCode).To(Equal(http.StatusOK), descUnexpectedHTTPStatus, rr.Body.String())

By("reading and parsing the response body")
body, err := io.ReadAll(rs.Body)
Expect(err).NotTo(HaveOccurred())

var response WorkspaceKindEnvelope
err = json.Unmarshal(body, &response)
Expect(err).NotTo(HaveOccurred())

By("verifying the resource was not created in the cluster")
notCreatedWorkspaceKind := &kubefloworgv1beta1.WorkspaceKind{}
err = k8sClient.Get(ctx, types.NamespacedName{Name: newWorkspaceKindName}, notCreatedWorkspaceKind)
Expect(err).To(HaveOccurred())
Expect(apierrors.IsNotFound(err)).To(BeTrue())
})

It("should return 400 for invalid YAML", func() {
invalidYAML := []byte("invalid: yaml: :")

By("creating the HTTP request")
req, err := http.NewRequest(http.MethodPost, AllWorkspaceKindsPath, bytes.NewReader(invalidYAML))
Expect(err).NotTo(HaveOccurred())

By("setting required headers")
req.Header.Set("Content-Type", MediaTypeYaml)
req.Header.Set(userIdHeader, adminUser)

By("executing CreateWorkspaceKindHandler")
ps := httprouter.Params{}
rr := httptest.NewRecorder()
a.CreateWorkspaceKindHandler(rr, req, ps)
rs := rr.Result()
defer rs.Body.Close()

By("verifying the HTTP response status code")
Expect(rs.StatusCode).To(Equal(http.StatusBadRequest), descUnexpectedHTTPStatus, rr.Body.String())
})

It("should return 415 for wrong content-type", func() {
By("creating the HTTP request")
req, err := http.NewRequest(http.MethodPost, AllWorkspaceKindsPath, bytes.NewReader(validYAML))
Expect(err).NotTo(HaveOccurred())

By("setting wrong content-type header")
req.Header.Set("Content-Type", MediaTypeJson)
req.Header.Set(userIdHeader, adminUser)

By("executing CreateWorkspaceKindHandler")
ps := httprouter.Params{}
rr := httptest.NewRecorder()
a.CreateWorkspaceKindHandler(rr, req, ps)
rs := rr.Result()
defer rs.Body.Close()

By("verifying the HTTP response status code")
Expect(rs.StatusCode).To(Equal(http.StatusUnsupportedMediaType), descUnexpectedHTTPStatus, rr.Body.String())
})

It("should return 400 for invalid dry-run value", func() {
By("creating the HTTP request with invalid dry-run value")
req, err := http.NewRequest(http.MethodPost, AllWorkspaceKindsPath+"?dry_run=invalid", bytes.NewReader(validYAML))
Expect(err).NotTo(HaveOccurred())

By("setting required headers")
req.Header.Set("Content-Type", MediaTypeYaml)
req.Header.Set(userIdHeader, adminUser)

By("executing CreateWorkspaceKindHandler")
ps := httprouter.Params{}
rr := httptest.NewRecorder()
a.CreateWorkspaceKindHandler(rr, req, ps)
rs := rr.Result()
defer rs.Body.Close()

By("verifying the HTTP response status code")
Expect(rs.StatusCode).To(Equal(http.StatusBadRequest), descUnexpectedHTTPStatus, rr.Body.String())
})
})
})
75 changes: 75 additions & 0 deletions workspaces/backend/devenv/backend.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
spec:
replicas: 1
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
serviceAccountName: backend-svc # custom service account
containers:
- name: backend
image: backend
ports:
- containerPort: 4000

---
# Service
apiVersion: v1
kind: Service
metadata:
name: backend
spec:
selector:
app: backend
ports:
- protocol: TCP
port: 80
targetPort: 4000

---
# ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: backend-svc
namespace: default

---
# ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: backend-role
rules:
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "list", "watch", "create", "update", "delete"]
- apiGroups: ["kubeflow.org"]
resources: ["workspacekinds"]
verbs: ["get", "list", "watch","create","update","delete"] # ["get", "list", "watch", "create", "update", "delete"]
- apiGroups: ["kubeflow.org"]
resources: ["workspaces"]
verbs: ["get", "list", "watch", "create", "update", "delete"]

---
# ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: backend-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: backend-role
subjects:
- kind: ServiceAccount
name: backend-svc
namespace: default
7 changes: 7 additions & 0 deletions workspaces/backend/devenv/dev.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM alpine:3.18

WORKDIR /app

COPY bin/backend /backend

ENTRYPOINT ["/backend"]
Loading
Loading