diff --git a/workspaces/backend/README.md b/workspaces/backend/README.md index 191fd68b..9dbdc88f 100644 --- a/workspaces/backend/README.md +++ b/workspaces/backend/README.md @@ -24,28 +24,33 @@ make run PORT=8000 ``` ### Endpoints -| URL Pattern | Handler | Action | -|------------------------------------------------------|----------------------|-------------------------------| -| GET /v1/healthcheck | HealthcheckHandler | Show application information. | -| GET /v1/spawner/{namespace}/workspaces | GetWorkspacesHandler | Get all Workspaces | -| POST /v1/spawner/{namespace}/workspaces | TBD | Create a Workspace | -| GET /v1/spawner/{namespace}/workspaces/{name} | TBD | Get a Workspace entity | -| PATCH /v1/spawner/{namespace}/workspaces/{name} | TBD | Patch a Workspace entity | -| PUT /v1/spawner/{namespace}/workspaces/{name} | TBD | Update a Workspace entity | -| DELETE /v1/spawner/{namespace}/workspaces/{name} | TBD | Delete a Workspace entity | -| GET /v1/spawner/{namespace}/workspacekinds | TDB | Get all WorkspaceKind | -| POST /v1/spawner/{namespace}/workspacekinds | TDB | Create a WorkspaceKind | -| GET /v1/spawner/{namespace}/workspacekinds/{name} | TBD | Get a WorkspaceKind entity | -| PATCH /v1/spawner/{namespace}/workspacekinds/{name} | TBD | Patch a WorkspaceKind entity | -| PUT /v1/spawner/{namespace}/workspacekinds/{name} | TBD | Update a WorkspaceKind entity | -| DELETE /v1/spawner/{namespace}/workspacekinds/{name} | TBD | Delete a WorkspaceKind entity | +| URL Pattern | Handler | Action | +|------------------------------------------|----------------------|-----------------------------------------| +| GET /v1/healthcheck | HealthcheckHandler | Show application information. | +| GET /v1/workspaces | GetWorkspacesHandler | Get all Workspaces | +| GET /v1/workspaces/{namespace} | GetWorkspacesHandler | Get all Workspaces from a namespace | +| POST /v1/workspaces/{namespace} | TBD | Create a Workspace in a given namespace | +| GET /v1/workspaces/{namespace}/{name} | TBD | Get a Workspace entity | +| PATCH /v1/workspaces/{namespace}/{name} | TBD | Patch a Workspace entity | +| PUT /v1/workspaces/{namespace}/{name} | TBD | Update a Workspace entity | +| DELETE /v1/workspaces/{namespace}/{name} | TBD | Delete a Workspace entity | +| GET /v1/workspacekinds | TBD | Get all WorkspaceKind | +| POST /v1/workspacekinds | TBD | Create a WorkspaceKind | +| GET /v1/workspacekinds/{name} | TBD | Get a WorkspaceKind entity | +| PATCH /v1/workspacekinds/{name} | TBD | Patch a WorkspaceKind entity | +| PUT /v1/workspacekinds/{name} | TBD | Update a WorkspaceKind entity | +| DELETE /v1/workspacekinds/{name} | TBD | Delete a WorkspaceKind entity | ### Sample local calls ``` # GET /v1/healthcheck -curl -i localhost:4000/api/v1/healthcheck/ +curl -i localhost:4000/api/v1/healthcheck ``` -`````` -# GET /v1/spawner/{namespace}/workspace -curl -i localhost:4000/api/v1/spawner/{namespace}/workspaces +``` +# GET /v1/workspaces/ +curl -i localhost:4000/api/v1/workspaces +``` +``` +# GET /v1/workspaces/{namespace} +curl -i localhost:4000/api/v1/workspaces/default ``` diff --git a/workspaces/backend/api/app.go b/workspaces/backend/api/app.go index 2091a2e5..7f7f6b6e 100644 --- a/workspaces/backend/api/app.go +++ b/workspaces/backend/api/app.go @@ -29,11 +29,14 @@ import ( ) const ( - Version = "1.0.0" - HealthCheckPath = "/api/v1/healthcheck/" - NamespacePathParam = "namespace" - PathPrefix = "/api/v1/spawner/:namespace" - WorkspacesPath = PathPrefix + "/workspaces" + Version = "1.0.0" + PathPrefix = "/api/v1" + + HealthCheckPath = PathPrefix + "/healthcheck" + //workspaces + AllWorkspacesPath = PathPrefix + "/workspaces" + NamespacePathParam = "namespace" + WorkspacesByNamespacePath = AllWorkspacesPath + "/:" + NamespacePathParam ) type App struct { @@ -63,7 +66,8 @@ func (a *App) Routes() http.Handler { router.MethodNotAllowed = http.HandlerFunc(a.methodNotAllowedResponse) router.GET(HealthCheckPath, a.HealthcheckHandler) - router.GET(WorkspacesPath, a.GetWorkspacesHandler) + router.GET(AllWorkspacesPath, a.GetWorkspacesHandler) + router.GET(WorkspacesByNamespacePath, a.GetWorkspacesHandler) return a.RecoverPanic(a.enableCORS(router)) } diff --git a/workspaces/backend/api/workspaces_handler.go b/workspaces/backend/api/workspaces_handler.go index 4494dabd..bcfd3017 100644 --- a/workspaces/backend/api/workspaces_handler.go +++ b/workspaces/backend/api/workspaces_handler.go @@ -17,6 +17,7 @@ limitations under the License. package api import ( + "github.com/kubeflow/notebooks/workspaces/backend/internal/models" "net/http" "github.com/julienschmidt/httprouter" @@ -26,7 +27,14 @@ func (a *App) GetWorkspacesHandler(w http.ResponseWriter, r *http.Request, ps ht namespace := ps.ByName(NamespacePathParam) - workspaces, err := a.repositories.Workspace.GetWorkspaces(r.Context(), namespace) + var workspaces []models.WorkspaceModel + var err error + if namespace == "" { + workspaces, err = a.repositories.Workspace.GetAllWorkspaces(r.Context()) + } else { + workspaces, err = a.repositories.Workspace.GetWorkspaces(r.Context(), namespace) + } + if err != nil { a.serverErrorResponse(w, r, err) return diff --git a/workspaces/backend/api/workspaces_handler_test.go b/workspaces/backend/api/workspaces_handler_test.go index 480fe8e5..0daabcb8 100644 --- a/workspaces/backend/api/workspaces_handler_test.go +++ b/workspaces/backend/api/workspaces_handler_test.go @@ -26,6 +26,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "io" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "net/http" @@ -34,9 +35,10 @@ import ( ) var _ = Describe("Workspaces Handler", func() { - Context("with existing workspaces", Serial, Ordered, func() { + Context("with existing workspaces", Ordered, func() { - const namespaceName = "default" + const namespaceName1 = "namespace1" + const namespaceName2 = "namespace2" var ( a App @@ -44,6 +46,8 @@ var _ = Describe("Workspaces Handler", func() { workspaceKey1 types.NamespacedName workspaceName2 string workspaceKey2 types.NamespacedName + workspaceName3 string + workspaceKey3 types.NamespacedName workspaceKindName string ) @@ -51,6 +55,7 @@ var _ = Describe("Workspaces Handler", func() { uniqueName := "wsk-update-test" workspaceName1 = fmt.Sprintf("workspace1-%s", uniqueName) workspaceName2 = fmt.Sprintf("workspace2-%s", uniqueName) + workspaceName3 = fmt.Sprintf("workspace3-%s", uniqueName) workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName) repos := repositories.NewRepositories(k8sClient) @@ -61,40 +66,69 @@ var _ = Describe("Workspaces Handler", func() { repositories: repos, } + By("creating namespaces") + namespace1 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespaceName1, + }, + } + Expect(k8sClient.Create(ctx, namespace1)).To(Succeed()) + + namespace2 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespaceName2, + }, + } + Expect(k8sClient.Create(ctx, namespace2)).To(Succeed()) + By("creating a WorkspaceKind") workspaceKind := NewExampleWorkspaceKind(workspaceKindName) Expect(k8sClient.Create(ctx, workspaceKind)).To(Succeed()) - By("creating the Workspace1") - workspace1 := NewExampleWorkspace(workspaceName1, namespaceName, workspaceKindName) + By("creating the Workspace1 at namespaceName1") + workspace1 := NewExampleWorkspace(workspaceName1, namespaceName1, workspaceKindName) Expect(k8sClient.Create(ctx, workspace1)).To(Succeed()) - workspaceKey1 = types.NamespacedName{Name: workspaceName1, Namespace: namespaceName} + workspaceKey1 = types.NamespacedName{Name: workspaceName1, Namespace: namespaceName1} - By("creating the Workspace2") - workspace2 := NewExampleWorkspace(workspaceName2, namespaceName, workspaceKindName) + By("creating the Workspace2 at namespaceName1") + workspace2 := NewExampleWorkspace(workspaceName2, namespaceName1, workspaceKindName) Expect(k8sClient.Create(ctx, workspace2)).To(Succeed()) - workspaceKey2 = types.NamespacedName{Name: workspaceName2, Namespace: namespaceName} + workspaceKey2 = types.NamespacedName{Name: workspaceName2, Namespace: namespaceName1} + + By("creating the Workspace3 at namespaceName2") + workspace3 := NewExampleWorkspace(workspaceName3, namespaceName2, workspaceKindName) + Expect(k8sClient.Create(ctx, workspace3)).To(Succeed()) + workspaceKey3 = types.NamespacedName{Name: workspaceName3, Namespace: namespaceName2} }) AfterAll(func() { - By("deleting the Workspace1") + By("deleting the Workspace1 at namespaceName1") workspace1 := &kubefloworgv1beta1.Workspace{ ObjectMeta: metav1.ObjectMeta{ Name: workspaceName1, - Namespace: namespaceName, + Namespace: namespaceName1, }, } Expect(k8sClient.Delete(ctx, workspace1)).To(Succeed()) - By("deleting the Workspace2") + By("deleting the Workspace2 at namespaceName1") workspace2 := &kubefloworgv1beta1.Workspace{ ObjectMeta: metav1.ObjectMeta{ Name: workspaceName2, - Namespace: namespaceName, + Namespace: namespaceName1, }, } Expect(k8sClient.Delete(ctx, workspace2)).To(Succeed()) + By("deleting the Workspace3 at namespaceName2") + workspace3 := &kubefloworgv1beta1.Workspace{ + ObjectMeta: metav1.ObjectMeta{ + Name: workspaceName3, + Namespace: namespaceName2, + }, + } + Expect(k8sClient.Delete(ctx, workspace3)).To(Succeed()) + By("deleting the WorkspaceKind") workspaceKind := &kubefloworgv1beta1.WorkspaceKind{ ObjectMeta: metav1.ObjectMeta{ @@ -102,12 +136,83 @@ var _ = Describe("Workspaces Handler", func() { }, } Expect(k8sClient.Delete(ctx, workspaceKind)).To(Succeed()) + + By("deleting the namespace1") + namespace1 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespaceName1, + }, + } + Expect(k8sClient.Delete(ctx, namespace1)).To(Succeed()) + + By("deleting the namespace2") + namespace2 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespaceName2, + }, + } + Expect(k8sClient.Delete(ctx, namespace2)).To(Succeed()) + }) + + It("should retrieve the workspaces from all namespaces successfully", func() { + + By("creating the HTTP request") + req, err := http.NewRequest(http.MethodGet, WorkspacesByNamespacePath, nil) + Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request") + + By("executing GetWorkspacesHandler") + ps := httprouter.Params{} + rr := httptest.NewRecorder() + a.GetWorkspacesHandler(rr, req, ps) + rs := rr.Result() + defer rs.Body.Close() + + By("verifying the HTTP response status code") + Expect(rs.StatusCode).To(Equal(http.StatusOK), "Expected HTTP status 200 OK") + + By("reading the HTTP response body") + body, err := io.ReadAll(rs.Body) + Expect(err).NotTo(HaveOccurred(), "Failed to read HTTP response body") + + By("unmarshalling the response JSON") + var response Envelope + err = json.Unmarshal(body, &response) + Expect(err).NotTo(HaveOccurred(), "Error unmarshalling response JSON") + + By("checking if 'workspaces' key exists in the response") + workspacesData, ok := response["workspaces"] + Expect(ok).To(BeTrue(), "Response does not contain 'workspaces' key") + + By("converting workspacesData to JSON and back to []WorkspaceModel") + workspacesJSON, err := json.Marshal(workspacesData) + Expect(err).NotTo(HaveOccurred(), "Error marshalling workspaces repositories") + + var workspaces []models.WorkspaceModel + err = json.Unmarshal(workspacesJSON, &workspaces) + Expect(err).NotTo(HaveOccurred(), "Error unmarshalling workspaces JSON") + + By("asserting that the retrieved workspaces match the expected workspaces") + workspace1 := &kubefloworgv1beta1.Workspace{} + Expect(k8sClient.Get(ctx, workspaceKey1, workspace1)).To(Succeed()) + workspace2 := &kubefloworgv1beta1.Workspace{} + Expect(k8sClient.Get(ctx, workspaceKey2, workspace2)).To(Succeed()) + workspace3 := &kubefloworgv1beta1.Workspace{} + Expect(k8sClient.Get(ctx, workspaceKey3, workspace3)).To(Succeed()) + + expectedWorkspaces := []models.WorkspaceModel{ + models.NewWorkspaceModelFromWorkspace(workspace1), + models.NewWorkspaceModelFromWorkspace(workspace2), + models.NewWorkspaceModelFromWorkspace(workspace3), + } + Expect(workspaces).To(ConsistOf(expectedWorkspaces)) + }) - It("should retrieve the workspaces successfully", func() { + It("should retrieve the workspaces from namespaceName1 successfully", func() { By("creating the HTTP request") - path := strings.Replace(WorkspacesPath, ":"+NamespacePathParam, namespaceName, 1) + path := strings.Replace(WorkspacesByNamespacePath, ":"+NamespacePathParam, namespaceName1, 1) + fmt.Println(path) req, err := http.NewRequest(http.MethodGet, path, nil) Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request") @@ -115,7 +220,7 @@ var _ = Describe("Workspaces Handler", func() { ps := httprouter.Params{ httprouter.Param{ Key: NamespacePathParam, - Value: namespaceName, + Value: namespaceName1, }, } rr := httptest.NewRecorder() @@ -164,7 +269,7 @@ var _ = Describe("Workspaces Handler", func() { }) Context("when there are no workspaces", func() { - const namespace = "default" + const otherNamespace = "otherNamespace" var a App BeforeEach(func() { @@ -176,10 +281,9 @@ var _ = Describe("Workspaces Handler", func() { repositories: repos, } }) - It("should return an empty list of workspaces", func() { By("creating the HTTP request") - path := strings.Replace(WorkspacesPath, ":"+NamespacePathParam, namespace, 1) + path := strings.Replace(AllWorkspacesPath, ":"+NamespacePathParam, otherNamespace, 1) req, err := http.NewRequest(http.MethodGet, path, nil) Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request") @@ -187,7 +291,7 @@ var _ = Describe("Workspaces Handler", func() { ps := httprouter.Params{ httprouter.Param{ Key: NamespacePathParam, - Value: namespace, + Value: otherNamespace, }, } rr := httptest.NewRecorder() diff --git a/workspaces/backend/internal/models/workspaces.go b/workspaces/backend/internal/models/workspaces.go index fee2daaf..b46de083 100644 --- a/workspaces/backend/internal/models/workspaces.go +++ b/workspaces/backend/internal/models/workspaces.go @@ -25,6 +25,7 @@ import ( ) type WorkspaceModel struct { + Namespace string `json:"namespace"` Name string `json:"name"` Kind string `json:"kind"` Image string `json:"image"` @@ -39,12 +40,14 @@ func NewWorkspaceModelFromWorkspace(item *kubefloworgv1beta1.Workspace) Workspac t := time.Unix(item.Status.Activity.LastActivity, 0) formattedLastActivity := t.Format("2006-01-02 15:04:05 MST") - mountPaths := make([]string, 0, len(item.Spec.PodTemplate.Volumes.Data)) - for _, volume := range item.Spec.PodTemplate.Volumes.Data { - mountPaths = append(mountPaths, volume.MountPath) + mountPaths := make([]string, len(item.Spec.PodTemplate.Volumes.Data)) + + for i, volume := range item.Spec.PodTemplate.Volumes.Data { + mountPaths[i] = volume.MountPath } workspaceModel := WorkspaceModel{ + Namespace: item.Namespace, Name: item.ObjectMeta.Name, Kind: item.Spec.Kind, Image: item.Spec.PodTemplate.Options.ImageConfig, diff --git a/workspaces/backend/internal/repositories/workspaces.go b/workspaces/backend/internal/repositories/workspaces.go index 81f523bf..1950da9e 100644 --- a/workspaces/backend/internal/repositories/workspaces.go +++ b/workspaces/backend/internal/repositories/workspaces.go @@ -43,11 +43,30 @@ func (r *WorkspaceRepository) GetWorkspaces(ctx context.Context, namespace strin return nil, err } - workspacesModels := make([]models.WorkspaceModel, 0, len(workspaceList.Items)) + workspacesModels := make([]models.WorkspaceModel, len(workspaceList.Items)) - for _, item := range workspaceList.Items { + for i, item := range workspaceList.Items { workspaceModel := models.NewWorkspaceModelFromWorkspace(&item) - workspacesModels = append(workspacesModels, workspaceModel) + workspacesModels[i] = workspaceModel + } + + return workspacesModels, nil +} + +func (r *WorkspaceRepository) GetAllWorkspaces(ctx context.Context) ([]models.WorkspaceModel, error) { + + workspaceList := &kubefloworgv1beta1.WorkspaceList{} + + err := r.client.List(ctx, workspaceList) + if err != nil { + return nil, err + } + + workspacesModels := make([]models.WorkspaceModel, len(workspaceList.Items)) + + for i, item := range workspaceList.Items { + workspaceModel := models.NewWorkspaceModelFromWorkspace(&item) + workspacesModels[i] = workspaceModel } return workspacesModels, nil