Skip to content

Commit

Permalink
feat(ws): Notebooks 2.0 // Backend // List Workspaces API - II
Browse files Browse the repository at this point in the history
In this PR:
- FUP for Notebooks 2.0 // Backend // List Workspaces API (#60) review
- Create /api/v1/workspaces to return all workspaces
- Review API endpoints as requested

Signed-off-by: Eder Ignatowicz <ignatowicz@gmail.com>
  • Loading branch information
ederign committed Sep 26, 2024
1 parent f852c16 commit 3d2dac8
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 51 deletions.
43 changes: 24 additions & 19 deletions workspaces/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
16 changes: 10 additions & 6 deletions workspaces/backend/api/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
}
10 changes: 9 additions & 1 deletion workspaces/backend/api/workspaces_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
142 changes: 123 additions & 19 deletions workspaces/backend/api/workspaces_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -34,23 +35,27 @@ 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
workspaceName1 string
workspaceKey1 types.NamespacedName
workspaceName2 string
workspaceKey2 types.NamespacedName
workspaceName3 string
workspaceKey3 types.NamespacedName
workspaceKindName string
)

BeforeAll(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)
Expand All @@ -61,61 +66,161 @@ 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{
Name: workspaceKindName,
},
}
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")

By("executing GetWorkspacesHandler")
ps := httprouter.Params{
httprouter.Param{
Key: NamespacePathParam,
Value: namespaceName,
Value: namespaceName1,
},
}
rr := httptest.NewRecorder()
Expand Down Expand Up @@ -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() {
Expand All @@ -176,18 +281,17 @@ 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")

By("executing GetWorkspacesHandler")
ps := httprouter.Params{
httprouter.Param{
Key: NamespacePathParam,
Value: namespace,
Value: otherNamespace,
},
}
rr := httptest.NewRecorder()
Expand Down
9 changes: 6 additions & 3 deletions workspaces/backend/internal/models/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
)

type WorkspaceModel struct {
Namespace string `json:"namespace"`
Name string `json:"name"`
Kind string `json:"kind"`
Image string `json:"image"`
Expand All @@ -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,
Expand Down
Loading

0 comments on commit 3d2dac8

Please sign in to comment.