From bed4affc5f423cfa7fc6de23161420945d502729 Mon Sep 17 00:00:00 2001 From: mohamedch7 <121046693+mohamedch7@users.noreply.github.com> Date: Thu, 13 Feb 2025 16:13:26 +0100 Subject: [PATCH 1/5] feat(ws): Add GET workspace yaml endpoint Signed-off-by: mohamedch7 <121046693+mohamedch7@users.noreply.github.com> --- workspaces/backend/api/app.go | 3 + .../backend/api/workspace_yaml_handler.go | 52 ++++++++ .../api/workspace_yaml_handler_test.go | 125 ++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 workspaces/backend/api/workspace_yaml_handler.go create mode 100644 workspaces/backend/api/workspace_yaml_handler_test.go diff --git a/workspaces/backend/api/app.go b/workspaces/backend/api/app.go index 5e5dc233..6827fad7 100644 --- a/workspaces/backend/api/app.go +++ b/workspaces/backend/api/app.go @@ -44,6 +44,8 @@ const ( AllWorkspacesPath = PathPrefix + "/workspaces" WorkspacesByNamespacePath = AllWorkspacesPath + "/:" + NamespacePathParam WorkspacesByNamePath = AllWorkspacesPath + "/:" + NamespacePathParam + "/:" + ResourceNamePathParam + WorkspaceDetailsPrefix = WorkspacesByNamePath + "/details" + WorkspaceYAMLPath = WorkspaceDetailsPrefix + "/yaml" // workspacekinds AllWorkspaceKindsPath = PathPrefix + "/workspacekinds" @@ -97,6 +99,7 @@ func (a *App) Routes() http.Handler { router.GET(WorkspacesByNamePath, a.GetWorkspaceHandler) router.POST(WorkspacesByNamespacePath, a.CreateWorkspaceHandler) router.DELETE(WorkspacesByNamePath, a.DeleteWorkspaceHandler) + router.GET(WorkspaceYAMLPath, a.GetWorkspaceYAMLHandler) // workspacekinds router.GET(AllWorkspaceKindsPath, a.GetWorkspaceKindsHandler) diff --git a/workspaces/backend/api/workspace_yaml_handler.go b/workspaces/backend/api/workspace_yaml_handler.go new file mode 100644 index 00000000..374b5328 --- /dev/null +++ b/workspaces/backend/api/workspace_yaml_handler.go @@ -0,0 +1,52 @@ +package api + +import ( + "fmt" + "net/http" + + "github.com/julienschmidt/httprouter" + + "errors" + + "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/workspaces" + "sigs.k8s.io/yaml" +) + +type WorkspaceYAMLEnvelope struct { + Data string `json:"data"` +} + +func (a *App) GetWorkspaceYAMLHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + namespace := ps.ByName(NamespacePathParam) + workspaceName := ps.ByName(ResourceNamePathParam) + + if namespace == "" || workspaceName == "" { + a.serverErrorResponse(w, r, fmt.Errorf("namespace or workspace name is empty")) + return + } + + workspace, err := a.repositories.Workspace.GetWorkspace(r.Context(), namespace, workspaceName) + if err != nil { + if errors.Is(err, workspaces.ErrWorkspaceNotFound) { + a.notFoundResponse(w, r) + return + } + a.serverErrorResponse(w, r, err) + return + } + + yamlBytes, err := yaml.Marshal(workspace) + if err != nil { + a.serverErrorResponse(w, r, err) + return + } + + response := WorkspaceYAMLEnvelope{ + Data: string(yamlBytes), + } + + err = a.WriteJSON(w, http.StatusOK, response, nil) + if err != nil { + a.serverErrorResponse(w, r, err) + } +} diff --git a/workspaces/backend/api/workspace_yaml_handler_test.go b/workspaces/backend/api/workspace_yaml_handler_test.go new file mode 100644 index 00000000..2a03a3da --- /dev/null +++ b/workspaces/backend/api/workspace_yaml_handler_test.go @@ -0,0 +1,125 @@ +package api + +import ( + "encoding/json" + "fmt" + "log/slog" + "net/http" + "net/http/httptest" + "os" + + "github.com/julienschmidt/httprouter" + "github.com/kubeflow/notebooks/workspaces/backend/internal/config" + "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories" + kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("Workspace YAML Handler", Ordered, func() { + const namespaceName = "namespace-yaml" + + var ( + a *App + workspace *kubefloworgv1beta1.Workspace + workspaceKey types.NamespacedName + workspaceKindName string + ) + + BeforeAll(func() { + uniqueName := "wsk-yaml-test" + workspaceName := fmt.Sprintf("workspace-%s", uniqueName) + workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName) + + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + repos := repositories.NewRepositories(k8sClient) + a = &App{ + Config: &config.EnvConfig{ + Port: 4000, + }, + repositories: repos, + logger: logger, + } + + By("creating namespace") + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespaceName, + }, + } + Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) + + By("creating a WorkspaceKind") + workspaceKind := NewExampleWorkspaceKind(workspaceKindName) + Expect(k8sClient.Create(ctx, workspaceKind)).To(Succeed()) + + By("creating the Workspace") + workspace = NewExampleWorkspace(workspaceName, namespaceName, workspaceKindName) + Expect(k8sClient.Create(ctx, workspace)).To(Succeed()) + workspaceKey = types.NamespacedName{Name: workspaceName, Namespace: namespaceName} + }) + + AfterAll(func() { + By("cleaning up resources") + workspace := &kubefloworgv1beta1.Workspace{} + if err := k8sClient.Get(ctx, workspaceKey, workspace); err == nil { + Expect(k8sClient.Delete(ctx, workspace)).To(Succeed()) + } + + workspaceKind := &kubefloworgv1beta1.WorkspaceKind{ + ObjectMeta: metav1.ObjectMeta{ + Name: workspaceKindName, + }, + } + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(workspaceKind), workspaceKind); err == nil { + Expect(k8sClient.Delete(ctx, workspaceKind)).To(Succeed()) + } + + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespaceName, + }, + } + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(namespace), namespace); err == nil { + Expect(k8sClient.Delete(ctx, namespace)).To(Succeed()) + } + }) + + It("should retrieve the workspace YAML successfully", func() { + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/workspaces/%s/%s/details/yaml", namespaceName, workspaceKey.Name), nil) + rr := httptest.NewRecorder() + + ps := httprouter.Params{ + {Key: "namespace", Value: namespaceName}, + {Key: "name", Value: workspaceKey.Name}, + } + + a.GetWorkspaceYAMLHandler(rr, req, ps) + + Expect(rr.Code).To(Equal(http.StatusOK)) + + var response WorkspaceYAMLEnvelope + Expect(json.NewDecoder(rr.Body).Decode(&response)).To(Succeed()) + + Expect(response.Data).To(ContainSubstring(fmt.Sprintf("name: %s", workspaceKey.Name))) + Expect(response.Data).To(ContainSubstring(fmt.Sprintf("namespace: %s", namespaceName))) + }) + + It("should return 404 when workspace doesn't exist", func() { + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/workspaces/%s/non-existent/details/yaml", namespaceName), nil) + rr := httptest.NewRecorder() + + ps := httprouter.Params{ + {Key: "namespace", Value: namespaceName}, + {Key: "name", Value: "non-existent"}, + } + + a.GetWorkspaceYAMLHandler(rr, req, ps) + + Expect(rr.Code).To(Equal(http.StatusNotFound)) + }) +}) From ad49f1762f6c7b4579c632ce7f0c0c40cdf43fa6 Mon Sep 17 00:00:00 2001 From: mohamedch7 <121046693+mohamedch7@users.noreply.github.com> Date: Thu, 13 Feb 2025 16:25:31 +0100 Subject: [PATCH 2/5] feat(ws): Add GET workspace yaml endpoint to README.md Signed-off-by: mohamedch7 <121046693+mohamedch7@users.noreply.github.com> --- workspaces/backend/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/workspaces/backend/README.md b/workspaces/backend/README.md index eb088313..e6cf38f9 100644 --- a/workspaces/backend/README.md +++ b/workspaces/backend/README.md @@ -43,6 +43,7 @@ make run PORT=8000 | PATCH /api/v1/workspaces/{namespace}/{name} | TBD | Patch a Workspace entity | | PUT /api/v1/workspaces/{namespace}/{name} | TBD | Update a Workspace entity | | DELETE /api/v1/workspaces/{namespace}/{name} | workspaces_handler | Delete a Workspace entity | +| GET /api/v1/workspaces/{namespace}/{name}/details/yaml | workspace_yaml_handler | Get the YAML details of a Workspace entity | | GET /api/v1/workspacekinds | workspacekinds_handler | Get all WorkspaceKind | | POST /api/v1/workspacekinds | TBD | Create a WorkspaceKind | | GET /api/v1/workspacekinds/{name} | workspacekinds_handler | Get a WorkspaceKind entity | From 5e7c92c4a45aa99916efce447376225eb0461bf1 Mon Sep 17 00:00:00 2001 From: mohamedch7 <121046693+mohamedch7@users.noreply.github.com> Date: Thu, 13 Feb 2025 16:43:14 +0100 Subject: [PATCH 3/5] fix(ws): Fix lint errors and typo Signed-off-by: mohamedch7 <121046693+mohamedch7@users.noreply.github.com> --- .../backend/api/workspace_yaml_handler.go | 15 ++++++++++++ .../api/workspace_yaml_handler_test.go | 23 +++++++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/workspaces/backend/api/workspace_yaml_handler.go b/workspaces/backend/api/workspace_yaml_handler.go index 374b5328..31b93748 100644 --- a/workspaces/backend/api/workspace_yaml_handler.go +++ b/workspaces/backend/api/workspace_yaml_handler.go @@ -1,3 +1,17 @@ +// Copyright 2024. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package api import ( @@ -9,6 +23,7 @@ import ( "errors" "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/workspaces" + "sigs.k8s.io/yaml" ) diff --git a/workspaces/backend/api/workspace_yaml_handler_test.go b/workspaces/backend/api/workspace_yaml_handler_test.go index 2a03a3da..bb34bd23 100644 --- a/workspaces/backend/api/workspace_yaml_handler_test.go +++ b/workspaces/backend/api/workspace_yaml_handler_test.go @@ -1,3 +1,17 @@ +// Copyright 2024. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package api import ( @@ -9,8 +23,6 @@ import ( "os" "github.com/julienschmidt/httprouter" - "github.com/kubeflow/notebooks/workspaces/backend/internal/config" - "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories" kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -18,6 +30,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kubeflow/notebooks/workspaces/backend/internal/config" + "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories" ) var _ = Describe("Workspace YAML Handler", Ordered, func() { @@ -90,7 +105,7 @@ var _ = Describe("Workspace YAML Handler", Ordered, func() { }) It("should retrieve the workspace YAML successfully", func() { - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/workspaces/%s/%s/details/yaml", namespaceName, workspaceKey.Name), nil) + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/workspaces/%s/%s/details/yaml", namespaceName, workspaceKey.Name), http.NoBody) rr := httptest.NewRecorder() ps := httprouter.Params{ @@ -110,7 +125,7 @@ var _ = Describe("Workspace YAML Handler", Ordered, func() { }) It("should return 404 when workspace doesn't exist", func() { - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/workspaces/%s/non-existent/details/yaml", namespaceName), nil) + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/workspaces/%s/non-existent/details/yaml", namespaceName), http.NoBody) rr := httptest.NewRecorder() ps := httprouter.Params{ From d5dda0a8cd0cf70fe72eb91be0936707710619e7 Mon Sep 17 00:00:00 2001 From: mohamedch7 <121046693+mohamedch7@users.noreply.github.com> Date: Thu, 27 Feb 2025 14:05:41 +0100 Subject: [PATCH 4/5] fix(ws): Update license header formatting in workspace_yaml_handler Signed-off-by: mohamedch7 <121046693+mohamedch7@users.noreply.github.com> --- .../backend/api/workspace_yaml_handler.go | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/workspaces/backend/api/workspace_yaml_handler.go b/workspaces/backend/api/workspace_yaml_handler.go index 31b93748..1673358d 100644 --- a/workspaces/backend/api/workspace_yaml_handler.go +++ b/workspaces/backend/api/workspace_yaml_handler.go @@ -1,16 +1,18 @@ -// Copyright 2024. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package api From d1ae418733bd44c48c38efa3d77be05f6c3af2f7 Mon Sep 17 00:00:00 2001 From: mohamedch7 <121046693+mohamedch7@users.noreply.github.com> Date: Fri, 28 Feb 2025 16:31:09 +0100 Subject: [PATCH 5/5] fix(ws): Get raw workspace yaml Signed-off-by: mohamedch7 <121046693+mohamedch7@users.noreply.github.com> --- workspaces/backend/api/workspace_yaml_handler.go | 11 +++++------ .../backend/internal/repositories/workspaces/repo.go | 11 +++++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/workspaces/backend/api/workspace_yaml_handler.go b/workspaces/backend/api/workspace_yaml_handler.go index 1673358d..295a8348 100644 --- a/workspaces/backend/api/workspace_yaml_handler.go +++ b/workspaces/backend/api/workspace_yaml_handler.go @@ -17,18 +17,17 @@ limitations under the License. package api import ( + "errors" "fmt" "net/http" "github.com/julienschmidt/httprouter" - - "errors" + "sigs.k8s.io/yaml" "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/workspaces" - - "sigs.k8s.io/yaml" ) +// WorkspaceYAMLEnvelope wraps the YAML string response type WorkspaceYAMLEnvelope struct { Data string `json:"data"` } @@ -42,7 +41,7 @@ func (a *App) GetWorkspaceYAMLHandler(w http.ResponseWriter, r *http.Request, ps return } - workspace, err := a.repositories.Workspace.GetWorkspace(r.Context(), namespace, workspaceName) + rawWorkspace, err := a.repositories.Workspace.GetRawWorkspace(r.Context(), namespace, workspaceName) if err != nil { if errors.Is(err, workspaces.ErrWorkspaceNotFound) { a.notFoundResponse(w, r) @@ -52,7 +51,7 @@ func (a *App) GetWorkspaceYAMLHandler(w http.ResponseWriter, r *http.Request, ps return } - yamlBytes, err := yaml.Marshal(workspace) + yamlBytes, err := yaml.Marshal(rawWorkspace) if err != nil { a.serverErrorResponse(w, r, err) return diff --git a/workspaces/backend/internal/repositories/workspaces/repo.go b/workspaces/backend/internal/repositories/workspaces/repo.go index 88134d1a..d153ad6f 100644 --- a/workspaces/backend/internal/repositories/workspaces/repo.go +++ b/workspaces/backend/internal/repositories/workspaces/repo.go @@ -202,3 +202,14 @@ func (r *WorkspaceRepository) DeleteWorkspace(ctx context.Context, namespace, wo return nil } + +func (r *WorkspaceRepository) GetRawWorkspace(ctx context.Context, namespace string, workspaceName string) (*kubefloworgv1beta1.Workspace, error) { + workspace := &kubefloworgv1beta1.Workspace{} + if err := r.client.Get(ctx, client.ObjectKey{Namespace: namespace, Name: workspaceName}, workspace); err != nil { + if apierrors.IsNotFound(err) { + return nil, ErrWorkspaceNotFound + } + return nil, err + } + return workspace, nil +}