feat(ws): refactor backend models and repositories (#192)
Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>
This commit is contained in:
parent
16f97f86d9
commit
710cbd4753
|
|
@ -77,16 +77,20 @@ func (a *App) Routes() http.Handler {
|
|||
router.NotFound = http.HandlerFunc(a.notFoundResponse)
|
||||
router.MethodNotAllowed = http.HandlerFunc(a.methodNotAllowedResponse)
|
||||
|
||||
router.GET(HealthCheckPath, a.HealthcheckHandler)
|
||||
// healthcheck
|
||||
router.GET(HealthCheckPath, a.GetHealthcheckHandler)
|
||||
|
||||
// namespaces
|
||||
router.GET(AllNamespacesPath, a.GetNamespacesHandler)
|
||||
|
||||
// workspaces
|
||||
router.GET(AllWorkspacesPath, a.GetWorkspacesHandler)
|
||||
router.GET(WorkspacesByNamespacePath, a.GetWorkspacesHandler)
|
||||
|
||||
router.GET(WorkspacesByNamePath, a.GetWorkspaceHandler)
|
||||
router.POST(WorkspacesByNamespacePath, a.CreateWorkspaceHandler)
|
||||
router.DELETE(WorkspacesByNamePath, a.DeleteWorkspaceHandler)
|
||||
|
||||
// workspacekinds
|
||||
router.GET(AllWorkspaceKindsPath, a.GetWorkspaceKindsHandler)
|
||||
router.GET(WorkspaceKindsByNamePath, a.GetWorkspaceKindHandler)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
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 (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/models"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories"
|
||||
)
|
||||
|
||||
func TestHealthCheckHandler(t *testing.T) {
|
||||
app := App{
|
||||
Config: config.EnvConfig{
|
||||
Port: 4000,
|
||||
},
|
||||
repositories: repositories.NewRepositories(k8sClient),
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req, err := http.NewRequest(http.MethodGet, HealthCheckPath, http.NoBody)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
app.HealthcheckHandler(rr, req, nil)
|
||||
rs := rr.Result()
|
||||
defer rs.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(rs.Body)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to read response body")
|
||||
}
|
||||
|
||||
var healthCheckRes models.HealthCheckModel
|
||||
err = json.Unmarshal(body, &healthCheckRes)
|
||||
if err != nil {
|
||||
t.Fatalf("Error unmarshalling response JSON: %v", err)
|
||||
}
|
||||
|
||||
expected := models.HealthCheckModel{
|
||||
Status: "available",
|
||||
SystemInfo: models.SystemInfo{
|
||||
Version: Version,
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, healthCheckRes)
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ import (
|
|||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
func (a *App) HealthcheckHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
func (a *App) GetHealthcheckHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
|
||||
healthCheck, err := a.repositories.HealthCheck.HealthCheck(Version)
|
||||
if err != nil {
|
||||
|
|
@ -34,5 +34,4 @@ func (a *App) HealthcheckHandler(w http.ResponseWriter, r *http.Request, ps http
|
|||
if err != nil {
|
||||
a.serverErrorResponse(w, r, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
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 (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
|
||||
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/health_check"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories"
|
||||
)
|
||||
|
||||
var _ = Describe("HealthCheck Handler", func() {
|
||||
var (
|
||||
a App
|
||||
)
|
||||
|
||||
Context("when backend is healthy", func() {
|
||||
|
||||
BeforeEach(func() {
|
||||
repos := repositories.NewRepositories(k8sClient)
|
||||
a = App{
|
||||
Config: config.EnvConfig{
|
||||
Port: 4000,
|
||||
},
|
||||
repositories: repos,
|
||||
}
|
||||
})
|
||||
|
||||
It("should return a health check response", func() {
|
||||
By("creating the HTTP request")
|
||||
req, err := http.NewRequest(http.MethodGet, HealthCheckPath, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("executing GetHealthCheckHandler")
|
||||
ps := httprouter.Params{}
|
||||
rr := httptest.NewRecorder()
|
||||
a.GetHealthcheckHandler(rr, req, ps)
|
||||
rs := rr.Result()
|
||||
defer rs.Body.Close()
|
||||
|
||||
By("verifying the HTTP response status code")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusOK))
|
||||
|
||||
By("reading the HTTP response body")
|
||||
body, err := io.ReadAll(rs.Body)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("unmarshalling the response JSON to HealthCheck")
|
||||
var response models.HealthCheck
|
||||
err = json.Unmarshal(body, &response)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("ensuring that the health check is as expected")
|
||||
expected := models.HealthCheck{
|
||||
Status: models.ServiceStatusHealthy,
|
||||
SystemInfo: models.SystemInfo{
|
||||
Version: Version,
|
||||
},
|
||||
}
|
||||
Expect(response).To(BeComparableTo(expected))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -21,10 +21,10 @@ import (
|
|||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/models"
|
||||
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/namespaces"
|
||||
)
|
||||
|
||||
type NamespacesEnvelope Envelope[[]models.NamespaceModel]
|
||||
type NamespacesEnvelope Envelope[[]models.Namespace]
|
||||
|
||||
func (a *App) GetNamespacesHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
|
||||
|
|
|
|||
|
|
@ -22,43 +22,40 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
. "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"
|
||||
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/models"
|
||||
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/namespaces"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories"
|
||||
)
|
||||
|
||||
var _ = Describe("Namespaces Handler", func() {
|
||||
var (
|
||||
a App
|
||||
testRouter *httprouter.Router
|
||||
a App
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
repos := repositories.NewRepositories(k8sClient)
|
||||
a = App{
|
||||
Config: config.EnvConfig{
|
||||
Port: 4000,
|
||||
},
|
||||
repositories: repos,
|
||||
}
|
||||
// NOTE: these tests assume a specific state of the cluster, so cannot be run in parallel with other tests.
|
||||
// therefore, we run them using the `Serial` Ginkgo decorators.
|
||||
Context("when namespaces exist", Serial, func() {
|
||||
|
||||
testRouter = httprouter.New()
|
||||
testRouter.GET("/api/namespaces", a.GetNamespacesHandler)
|
||||
})
|
||||
|
||||
Context("when namespaces exist", func() {
|
||||
const namespaceName1 = "namespaceone"
|
||||
const namespaceName2 = "namespacetwo"
|
||||
const namespaceName1 = "get-ns-test-ns1"
|
||||
const namespaceName2 = "get-ns-test-ns2"
|
||||
|
||||
BeforeEach(func() {
|
||||
By("creating namespaces")
|
||||
repos := repositories.NewRepositories(k8sClient)
|
||||
a = App{
|
||||
Config: config.EnvConfig{
|
||||
Port: 4000,
|
||||
},
|
||||
repositories: repos,
|
||||
}
|
||||
|
||||
By("creating Namespace 1")
|
||||
namespace1 := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespaceName1,
|
||||
|
|
@ -66,18 +63,17 @@ var _ = Describe("Namespaces Handler", func() {
|
|||
}
|
||||
Expect(k8sClient.Create(ctx, namespace1)).To(Succeed())
|
||||
|
||||
By("creating Namespace 2")
|
||||
namespace2 := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespaceName2,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, namespace2)).To(Succeed())
|
||||
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
By("deleting namespaces")
|
||||
By("deleting the namespace1")
|
||||
By("deleting Namespace 1")
|
||||
namespace1 := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespaceName1,
|
||||
|
|
@ -85,7 +81,7 @@ var _ = Describe("Namespaces Handler", func() {
|
|||
}
|
||||
Expect(k8sClient.Delete(ctx, namespace1)).To(Succeed())
|
||||
|
||||
By("deleting the namespace2")
|
||||
By("deleting Namespace 2")
|
||||
namespace2 := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespaceName2,
|
||||
|
|
@ -96,32 +92,40 @@ var _ = Describe("Namespaces Handler", func() {
|
|||
|
||||
It("should retrieve all namespaces successfully", func() {
|
||||
By("creating the HTTP request")
|
||||
req, err := http.NewRequest(http.MethodGet, "/api/namespaces", http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")
|
||||
req, err := http.NewRequest(http.MethodGet, AllNamespacesPath, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("executing GetNamespacesHandler")
|
||||
ps := httprouter.Params{}
|
||||
rr := httptest.NewRecorder()
|
||||
testRouter.ServeHTTP(rr, req)
|
||||
a.GetNamespacesHandler(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")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusOK))
|
||||
|
||||
By("reading the HTTP response body")
|
||||
body, err := io.ReadAll(rs.Body)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to read HTTP response body")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("unmarshalling the response JSON")
|
||||
By("unmarshalling the response JSON to NamespacesEnvelope")
|
||||
var response NamespacesEnvelope
|
||||
err = json.Unmarshal(body, &response)
|
||||
Expect(err).NotTo(HaveOccurred(), "Error unmarshalling response JSON")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("asserting that the created namespaces are in the response")
|
||||
By("getting the Namespaces from the Kubernetes API")
|
||||
namespace1 := &corev1.Namespace{}
|
||||
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: namespaceName1}, namespace1)).To(Succeed())
|
||||
namespace2 := &corev1.Namespace{}
|
||||
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: namespaceName2}, namespace2)).To(Succeed())
|
||||
|
||||
By("ensuring the response contains the expected Namespaces")
|
||||
// NOTE: we use `ContainElements` instead of `ConsistOf` because envtest creates some namespaces by default
|
||||
Expect(response.Data).To(ContainElements(
|
||||
models.NamespaceModel{Name: namespaceName1},
|
||||
models.NamespaceModel{Name: namespaceName2},
|
||||
), "Expected created namespaces to be in the response")
|
||||
models.NewNamespaceModelFromNamespace(namespace1),
|
||||
models.NewNamespaceModelFromNamespace(namespace2),
|
||||
))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -23,12 +23,13 @@ import (
|
|||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/models"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories"
|
||||
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspacekinds"
|
||||
repository "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/workspacekinds"
|
||||
)
|
||||
|
||||
type WorkspaceKindsEnvelope Envelope[[]models.WorkspaceKindModel]
|
||||
type WorkspaceKindEnvelope Envelope[models.WorkspaceKindModel]
|
||||
type WorkspaceKindsEnvelope Envelope[[]models.WorkspaceKind]
|
||||
|
||||
type WorkspaceKindEnvelope Envelope[models.WorkspaceKind]
|
||||
|
||||
func (a *App) GetWorkspaceKindHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
name := ps.ByName("name")
|
||||
|
|
@ -40,7 +41,7 @@ func (a *App) GetWorkspaceKindHandler(w http.ResponseWriter, r *http.Request, ps
|
|||
|
||||
workspaceKind, err := a.repositories.WorkspaceKind.GetWorkspaceKind(r.Context(), name)
|
||||
if err != nil {
|
||||
if errors.Is(err, repositories.ErrWorkspaceKindNotFound) {
|
||||
if errors.Is(err, repository.ErrWorkspaceKindNotFound) {
|
||||
a.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,28 +33,33 @@ import (
|
|||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/models"
|
||||
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspacekinds"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories"
|
||||
)
|
||||
|
||||
var _ = Describe("WorkspaceKinds Handler", func() {
|
||||
Context("with existing workspacekinds", Ordered, func() {
|
||||
|
||||
const namespaceName1 = "namespace-kind"
|
||||
// NOTE: the tests in this context work on the same resources, they must be run in order.
|
||||
// also, they assume a specific state of the cluster, so cannot be run in parallel with other tests.
|
||||
// therefore, we run them using the `Ordered` and `Serial` Ginkgo decorators.
|
||||
Context("with existing WorkspaceKinds", Serial, Ordered, func() {
|
||||
|
||||
const namespaceName1 = "wsk-exist-test-ns1"
|
||||
|
||||
var (
|
||||
a App
|
||||
a App
|
||||
|
||||
workspaceKind1Name string
|
||||
workspaceKind2Name string
|
||||
workspaceKind1Key types.NamespacedName
|
||||
workspaceKind2Name string
|
||||
workspaceKind2Key types.NamespacedName
|
||||
)
|
||||
|
||||
BeforeAll(func() {
|
||||
uniqueName := "wskind-update-test"
|
||||
workspaceKind1Name = fmt.Sprintf("workspacekind1-%s", uniqueName)
|
||||
uniqueName := "wsk-exist-test"
|
||||
workspaceKind1Name = fmt.Sprintf("workspacekind-1-%s", uniqueName)
|
||||
workspaceKind1Key = types.NamespacedName{Name: workspaceKind1Name}
|
||||
workspaceKind2Name = fmt.Sprintf("workspacekind2-%s", uniqueName)
|
||||
workspaceKind2Name = fmt.Sprintf("workspacekind-2-%s", uniqueName)
|
||||
workspaceKind2Key = types.NamespacedName{Name: workspaceKind2Name}
|
||||
|
||||
repos := repositories.NewRepositories(k8sClient)
|
||||
|
|
@ -65,7 +70,7 @@ var _ = Describe("WorkspaceKinds Handler", func() {
|
|||
repositories: repos,
|
||||
}
|
||||
|
||||
By("creating namespaces")
|
||||
By("creating Namespace 1")
|
||||
namespace1 := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespaceName1,
|
||||
|
|
@ -73,19 +78,17 @@ var _ = Describe("WorkspaceKinds Handler", func() {
|
|||
}
|
||||
Expect(k8sClient.Create(ctx, namespace1)).To(Succeed())
|
||||
|
||||
By("creating a WorkspaceKind1")
|
||||
By("creating WorkspaceKind 1")
|
||||
workspaceKind1 := NewExampleWorkspaceKind(workspaceKind1Name)
|
||||
Expect(k8sClient.Create(ctx, workspaceKind1)).To(Succeed())
|
||||
|
||||
By("creating a WorkspaceKind1")
|
||||
By("creating WorkspaceKind 2")
|
||||
workspaceKind2 := NewExampleWorkspaceKind(workspaceKind2Name)
|
||||
Expect(k8sClient.Create(ctx, workspaceKind2)).To(Succeed())
|
||||
|
||||
})
|
||||
|
||||
AfterAll(func() {
|
||||
|
||||
By("deleting the WorkspaceKind1")
|
||||
By("deleting WorkspaceKind 1")
|
||||
workspaceKind1 := &kubefloworgv1beta1.WorkspaceKind{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: workspaceKind1Name,
|
||||
|
|
@ -93,7 +96,7 @@ var _ = Describe("WorkspaceKinds Handler", func() {
|
|||
}
|
||||
Expect(k8sClient.Delete(ctx, workspaceKind1)).To(Succeed())
|
||||
|
||||
By("deleting the WorkspaceKind2")
|
||||
By("deleting WorkspaceKind 2")
|
||||
workspaceKind2 := &kubefloworgv1beta1.WorkspaceKind{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: workspaceKind2Name,
|
||||
|
|
@ -101,7 +104,7 @@ var _ = Describe("WorkspaceKinds Handler", func() {
|
|||
}
|
||||
Expect(k8sClient.Delete(ctx, workspaceKind2)).To(Succeed())
|
||||
|
||||
By("deleting the namespace1")
|
||||
By("deleting Namespace 1")
|
||||
namespace1 := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespaceName1,
|
||||
|
|
@ -111,10 +114,10 @@ var _ = Describe("WorkspaceKinds Handler", func() {
|
|||
|
||||
})
|
||||
|
||||
It("should retrieve the all workspacekinds successfully", func() {
|
||||
It("should retrieve the all WorkspaceKinds successfully", func() {
|
||||
By("creating the HTTP request")
|
||||
req, err := http.NewRequest(http.MethodGet, WorkspacesByNamespacePath, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")
|
||||
req, err := http.NewRequest(http.MethodGet, AllWorkspaceKindsPath, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("executing GetWorkspaceKindsHandler")
|
||||
ps := httprouter.Params{}
|
||||
|
|
@ -124,50 +127,46 @@ var _ = Describe("WorkspaceKinds Handler", func() {
|
|||
defer rs.Body.Close()
|
||||
|
||||
By("verifying the HTTP response status code")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusOK), "Expected HTTP status 200 OK")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusOK))
|
||||
|
||||
By("reading the HTTP response body")
|
||||
body, err := io.ReadAll(rs.Body)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to read HTTP response body")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("unmarshalling the response JSON")
|
||||
By("unmarshalling the response JSON to WorkspaceKindsEnvelope")
|
||||
var response WorkspaceKindsEnvelope
|
||||
err = json.Unmarshal(body, &response)
|
||||
Expect(err).NotTo(HaveOccurred(), "Error unmarshalling response JSON")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("retrieving workspaceKindsData in the response")
|
||||
workspaceKindsData := response.Data
|
||||
|
||||
By("converting workspaceKindsData to JSON and back to []WorkspaceKindsModel")
|
||||
workspaceKindsJSON, err := json.Marshal(workspaceKindsData)
|
||||
Expect(err).NotTo(HaveOccurred(), "Error marshaling workspaces repositories")
|
||||
|
||||
var workspaceKinds []models.WorkspaceKindModel
|
||||
err = json.Unmarshal(workspaceKindsJSON, &workspaceKinds)
|
||||
Expect(err).NotTo(HaveOccurred(), "Error unmarshalling workspaces JSON")
|
||||
|
||||
By("asserting that the retrieved workspaces kinds match the expected workspacekinds")
|
||||
By("getting the WorkspaceKinds from the Kubernetes API")
|
||||
workspacekind1 := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
Expect(k8sClient.Get(ctx, workspaceKind1Key, workspacekind1)).To(Succeed())
|
||||
workspacekind2 := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
Expect(k8sClient.Get(ctx, workspaceKind2Key, workspacekind2)).To(Succeed())
|
||||
|
||||
expectedWorkspaceKinds := []models.WorkspaceKindModel{
|
||||
By("ensuring the response contains the expected WorkspaceKinds")
|
||||
Expect(response.Data).To(ConsistOf(
|
||||
models.NewWorkspaceKindModelFromWorkspaceKind(workspacekind1),
|
||||
models.NewWorkspaceKindModelFromWorkspaceKind(workspacekind2),
|
||||
}
|
||||
Expect(workspaceKinds).To(ConsistOf(expectedWorkspaceKinds))
|
||||
))
|
||||
|
||||
By("ensuring the wrapped data can be marshaled to JSON and back to []WorkspaceKind")
|
||||
dataJSON, err := json.Marshal(response.Data)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to marshal data to JSON")
|
||||
var dataObject []models.WorkspaceKind
|
||||
err = json.Unmarshal(dataJSON, &dataObject)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to unmarshal JSON to []WorkspaceKind")
|
||||
})
|
||||
|
||||
It("should retrieve a single workspacekind successfully", func() {
|
||||
It("should retrieve a single WorkspaceKind successfully", func() {
|
||||
By("creating the HTTP request")
|
||||
path := strings.Replace(WorkspaceKindsByNamePath, ":name", workspaceKind1Name, 1)
|
||||
path := strings.Replace(WorkspaceKindsByNamePath, ":"+WorkspaceKindNamePathParam, workspaceKind1Name, 1)
|
||||
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("executing GetWorkspaceKindHandler")
|
||||
ps := httprouter.Params{
|
||||
httprouter.Param{Key: "name", Value: workspaceKind1Name},
|
||||
httprouter.Param{Key: WorkspaceKindNamePathParam, Value: workspaceKind1Name},
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
a.GetWorkspaceKindHandler(rr, req, ps)
|
||||
|
|
@ -175,31 +174,37 @@ var _ = Describe("WorkspaceKinds Handler", func() {
|
|||
defer rs.Body.Close()
|
||||
|
||||
By("verifying the HTTP response status code")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusOK), "Expected HTTP status 200 OK")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusOK))
|
||||
|
||||
By("reading the HTTP response body")
|
||||
body, err := io.ReadAll(rs.Body)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to read HTTP response body")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("unmarshalling the response JSON")
|
||||
By("unmarshalling the response JSON to WorkspaceKindEnvelope")
|
||||
var response WorkspaceKindEnvelope
|
||||
err = json.Unmarshal(body, &response)
|
||||
Expect(err).NotTo(HaveOccurred(), "Error unmarshalling response JSON")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("retrieving workspaceKindData in the response")
|
||||
workspaceKindData := response.Data
|
||||
|
||||
By("comparing the retrieved workspacekind with the expected")
|
||||
By("getting the WorkspaceKind from the Kubernetes API")
|
||||
workspacekind1 := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
Expect(k8sClient.Get(ctx, workspaceKind1Key, workspacekind1)).To(Succeed())
|
||||
|
||||
By("ensuring the response matches the expected WorkspaceKind")
|
||||
expectedWorkspaceKind := models.NewWorkspaceKindModelFromWorkspaceKind(workspacekind1)
|
||||
Expect(workspaceKindData).To(Equal(expectedWorkspaceKind))
|
||||
})
|
||||
Expect(response.Data).To(BeComparableTo(expectedWorkspaceKind))
|
||||
|
||||
By("ensuring the wrapped data can be marshaled to JSON and back")
|
||||
dataJSON, err := json.Marshal(response.Data)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to marshal data to JSON")
|
||||
var dataObject models.WorkspaceKind
|
||||
err = json.Unmarshal(dataJSON, &dataObject)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to unmarshal JSON to WorkspaceKind")
|
||||
})
|
||||
})
|
||||
|
||||
Context("when there are no workspacekinds ", func() {
|
||||
// NOTE: these tests assume a specific state of the cluster, so cannot be run in parallel with other tests.
|
||||
// therefore, we run them using the `Serial` Ginkgo decorators.
|
||||
Context("with no existing WorkspaceKinds", Serial, func() {
|
||||
|
||||
var a App
|
||||
|
||||
|
|
@ -212,10 +217,11 @@ var _ = Describe("WorkspaceKinds Handler", func() {
|
|||
repositories: repos,
|
||||
}
|
||||
})
|
||||
It("should return an empty list of workspacekinds", func() {
|
||||
|
||||
It("should return an empty list of WorkspaceKinds", func() {
|
||||
By("creating the HTTP request")
|
||||
req, err := http.NewRequest(http.MethodGet, AllWorkspaceKindsPath, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("executing GetWorkspacesHandler")
|
||||
ps := httprouter.Params{}
|
||||
|
|
@ -225,36 +231,32 @@ var _ = Describe("WorkspaceKinds Handler", func() {
|
|||
defer rs.Body.Close()
|
||||
|
||||
By("verifying the HTTP response status code")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusOK), "Expected HTTP status 200 OK")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusOK))
|
||||
|
||||
By("reading the HTTP response body")
|
||||
body, err := io.ReadAll(rs.Body)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to read HTTP response body")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("unmarshalling the response JSON")
|
||||
By("unmarshalling the response JSON to WorkspaceKindsEnvelope")
|
||||
var response WorkspaceKindsEnvelope
|
||||
err = json.Unmarshal(body, &response)
|
||||
Expect(err).NotTo(HaveOccurred(), "Error unmarshalling response JSON")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("asserting that the 'workspaces' list is empty")
|
||||
workspaceskindsJSON, err := json.Marshal(response.Data)
|
||||
Expect(err).NotTo(HaveOccurred(), "Error marshaling workspaces data")
|
||||
|
||||
var workspaceKinds []models.WorkspaceKindModel
|
||||
err = json.Unmarshal(workspaceskindsJSON, &workspaceKinds)
|
||||
Expect(err).NotTo(HaveOccurred(), "Error unmarshalling workspaces JSON")
|
||||
Expect(workspaceKinds).To(BeEmpty(), "Expected no workspaces in the response")
|
||||
By("ensuring that no WorkspaceKinds were returned")
|
||||
Expect(response.Data).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("should return 404 for a non-existent workspacekind", func() {
|
||||
By("creating the HTTP request for a non-existent workspacekind")
|
||||
path := strings.Replace(WorkspaceKindsByNamePath, ":name", "non-existent-workspacekind", 1)
|
||||
It("should return 404 for a non-existent WorkspaceKind", func() {
|
||||
missingWorkspaceKindName := "non-existent-workspacekind"
|
||||
|
||||
By("creating the HTTP request")
|
||||
path := strings.Replace(WorkspaceKindsByNamePath, ":"+WorkspaceNamePathParam, missingWorkspaceKindName, 1)
|
||||
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("executing GetWorkspaceKindHandler")
|
||||
ps := httprouter.Params{
|
||||
httprouter.Param{Key: "name", Value: "non-existent-workspacekind"},
|
||||
httprouter.Param{Key: WorkspaceNamePathParam, Value: missingWorkspaceKindName},
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
a.GetWorkspaceKindHandler(rr, req, ps)
|
||||
|
|
@ -262,7 +264,7 @@ var _ = Describe("WorkspaceKinds Handler", func() {
|
|||
defer rs.Body.Close()
|
||||
|
||||
By("verifying the HTTP response status code")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusNotFound), "Expected HTTP status 404 Not Found")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusNotFound))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -24,18 +24,19 @@ import (
|
|||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/models"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories"
|
||||
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspaces"
|
||||
repository "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/workspaces"
|
||||
)
|
||||
|
||||
type WorkspacesEnvelope Envelope[[]models.WorkspaceModel]
|
||||
type WorkspaceEnvelope Envelope[models.WorkspaceModel]
|
||||
type WorkspacesEnvelope Envelope[[]models.Workspace]
|
||||
|
||||
type WorkspaceEnvelope Envelope[models.Workspace]
|
||||
|
||||
func (a *App) GetWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
namespace := ps.ByName(NamespacePathParam)
|
||||
workspaceName := ps.ByName(WorkspaceNamePathParam)
|
||||
|
||||
var workspace models.WorkspaceModel
|
||||
var workspace models.Workspace
|
||||
var err error
|
||||
if namespace == "" {
|
||||
a.serverErrorResponse(w, r, fmt.Errorf("namespace is nil"))
|
||||
|
|
@ -48,7 +49,7 @@ func (a *App) GetWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps htt
|
|||
|
||||
workspace, err = a.repositories.Workspace.GetWorkspace(r.Context(), namespace, workspaceName)
|
||||
if err != nil {
|
||||
if errors.Is(err, repositories.ErrWorkspaceNotFound) {
|
||||
if errors.Is(err, repository.ErrWorkspaceNotFound) {
|
||||
a.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
|
|
@ -70,7 +71,7 @@ func (a *App) GetWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps htt
|
|||
func (a *App) GetWorkspacesHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
namespace := ps.ByName(NamespacePathParam)
|
||||
|
||||
var workspaces []models.WorkspaceModel
|
||||
var workspaces []models.Workspace
|
||||
var err error
|
||||
if namespace == "" {
|
||||
workspaces, err = a.repositories.Workspace.GetAllWorkspaces(r.Context())
|
||||
|
|
@ -100,7 +101,7 @@ func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps
|
|||
return
|
||||
}
|
||||
|
||||
workspaceModel := &models.WorkspaceModel{}
|
||||
workspaceModel := &models.Workspace{}
|
||||
if err := json.NewDecoder(r.Body).Decode(workspaceModel); err != nil {
|
||||
a.serverErrorResponse(w, r, fmt.Errorf("error decoding JSON: %w", err))
|
||||
return
|
||||
|
|
@ -142,7 +143,7 @@ func (a *App) DeleteWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps
|
|||
|
||||
err := a.repositories.Workspace.DeleteWorkspace(r.Context(), namespace, workspaceName)
|
||||
if err != nil {
|
||||
if errors.Is(err, repositories.ErrWorkspaceNotFound) {
|
||||
if errors.Is(err, repository.ErrWorkspaceNotFound) {
|
||||
a.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,37 +29,50 @@ 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/utils/ptr"
|
||||
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/models"
|
||||
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspaces"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories"
|
||||
)
|
||||
|
||||
var _ = Describe("Workspaces Handler", func() {
|
||||
Context("with existing workspaces", Ordered, func() {
|
||||
|
||||
const namespaceName1 = "namespace1"
|
||||
const namespaceName2 = "namespace2"
|
||||
// NOTE: the tests in this context work on the same resources, they must be run in order.
|
||||
// also, they assume a specific state of the cluster, so cannot be run in parallel with other tests.
|
||||
// therefore, we run them using the `Ordered` and `Serial` Ginkgo decorators.
|
||||
Context("with existing Workspaces", Serial, Ordered, func() {
|
||||
|
||||
const namespaceName1 = "ws-exist-ns1"
|
||||
const namespaceName2 = "ws-exist-ns2"
|
||||
|
||||
var (
|
||||
a App
|
||||
workspaceName1 string
|
||||
workspaceKey1 types.NamespacedName
|
||||
workspaceName2 string
|
||||
workspaceKey2 types.NamespacedName
|
||||
workspaceName3 string
|
||||
workspaceKey3 types.NamespacedName
|
||||
a App
|
||||
|
||||
workspaceName1 string
|
||||
workspaceKey1 types.NamespacedName
|
||||
workspaceName2 string
|
||||
workspaceKey2 types.NamespacedName
|
||||
workspaceName3 string
|
||||
workspaceKey3 types.NamespacedName
|
||||
|
||||
workspaceKindName string
|
||||
workspaceKindKey types.NamespacedName
|
||||
)
|
||||
|
||||
BeforeAll(func() {
|
||||
uniqueName := "wsk-update-test"
|
||||
workspaceName1 = fmt.Sprintf("workspace1-%s", uniqueName)
|
||||
workspaceName2 = fmt.Sprintf("workspace2-%s", uniqueName)
|
||||
workspaceName3 = fmt.Sprintf("workspace3-%s", uniqueName)
|
||||
uniqueName := "ws-exist-test"
|
||||
workspaceName1 = fmt.Sprintf("workspace-1-%s", uniqueName)
|
||||
workspaceKey1 = types.NamespacedName{Name: workspaceName1, Namespace: namespaceName1}
|
||||
workspaceName2 = fmt.Sprintf("workspace-2-%s", uniqueName)
|
||||
workspaceKey2 = types.NamespacedName{Name: workspaceName2, Namespace: namespaceName1}
|
||||
workspaceName3 = fmt.Sprintf("workspace-3-%s", uniqueName)
|
||||
workspaceKey3 = types.NamespacedName{Name: workspaceName3, Namespace: namespaceName2}
|
||||
workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName)
|
||||
workspaceKindKey = types.NamespacedName{Name: workspaceKindName}
|
||||
|
||||
repos := repositories.NewRepositories(k8sClient)
|
||||
a = App{
|
||||
|
|
@ -69,7 +82,7 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
repositories: repos,
|
||||
}
|
||||
|
||||
By("creating namespaces")
|
||||
By("creating Namespace 1")
|
||||
namespace1 := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespaceName1,
|
||||
|
|
@ -77,6 +90,7 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
}
|
||||
Expect(k8sClient.Create(ctx, namespace1)).To(Succeed())
|
||||
|
||||
By("creating Namespace 2")
|
||||
namespace2 := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespaceName2,
|
||||
|
|
@ -88,24 +102,21 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
workspaceKind := NewExampleWorkspaceKind(workspaceKindName)
|
||||
Expect(k8sClient.Create(ctx, workspaceKind)).To(Succeed())
|
||||
|
||||
By("creating the Workspace1 at namespaceName1")
|
||||
By("creating Workspace 1 in Namespace 1")
|
||||
workspace1 := NewExampleWorkspace(workspaceName1, namespaceName1, workspaceKindName)
|
||||
Expect(k8sClient.Create(ctx, workspace1)).To(Succeed())
|
||||
workspaceKey1 = types.NamespacedName{Name: workspaceName1, Namespace: namespaceName1}
|
||||
|
||||
By("creating the Workspace2 at namespaceName1")
|
||||
By("creating Workspace 2 in Namespace 1")
|
||||
workspace2 := NewExampleWorkspace(workspaceName2, namespaceName1, workspaceKindName)
|
||||
Expect(k8sClient.Create(ctx, workspace2)).To(Succeed())
|
||||
workspaceKey2 = types.NamespacedName{Name: workspaceName2, Namespace: namespaceName1}
|
||||
|
||||
By("creating the Workspace3 at namespaceName2")
|
||||
By("creating Workspace 3 in Namespace 2")
|
||||
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 at namespaceName1")
|
||||
By("deleting Workspace 1 from Namespace 1")
|
||||
workspace1 := &kubefloworgv1beta1.Workspace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: workspaceName1,
|
||||
|
|
@ -114,7 +125,7 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
}
|
||||
Expect(k8sClient.Delete(ctx, workspace1)).To(Succeed())
|
||||
|
||||
By("deleting the Workspace2 at namespaceName1")
|
||||
By("deleting Workspace 2 from Namespace 1")
|
||||
workspace2 := &kubefloworgv1beta1.Workspace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: workspaceName2,
|
||||
|
|
@ -123,7 +134,7 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
}
|
||||
Expect(k8sClient.Delete(ctx, workspace2)).To(Succeed())
|
||||
|
||||
By("deleting the Workspace3 at namespaceName2")
|
||||
By("deleting Workspace 3 from Namespace 2")
|
||||
workspace3 := &kubefloworgv1beta1.Workspace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: workspaceName3,
|
||||
|
|
@ -132,7 +143,7 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
}
|
||||
Expect(k8sClient.Delete(ctx, workspace3)).To(Succeed())
|
||||
|
||||
By("deleting the WorkspaceKind")
|
||||
By("deleting WorkspaceKind")
|
||||
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: workspaceKindName,
|
||||
|
|
@ -140,7 +151,7 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
}
|
||||
Expect(k8sClient.Delete(ctx, workspaceKind)).To(Succeed())
|
||||
|
||||
By("deleting the namespace1")
|
||||
By("deleting Namespace 1")
|
||||
namespace1 := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespaceName1,
|
||||
|
|
@ -148,7 +159,7 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
}
|
||||
Expect(k8sClient.Delete(ctx, namespace1)).To(Succeed())
|
||||
|
||||
By("deleting the namespace2")
|
||||
By("deleting Namespace 2")
|
||||
namespace2 := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespaceName2,
|
||||
|
|
@ -157,11 +168,10 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
Expect(k8sClient.Delete(ctx, namespace2)).To(Succeed())
|
||||
})
|
||||
|
||||
It("should retrieve the workspaces from all namespaces successfully", func() {
|
||||
|
||||
It("should retrieve Workspaces from all namespaces successfully", func() {
|
||||
By("creating the HTTP request")
|
||||
req, err := http.NewRequest(http.MethodGet, WorkspacesByNamespacePath, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")
|
||||
req, err := http.NewRequest(http.MethodGet, AllWorkspacesPath, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("executing GetWorkspacesHandler")
|
||||
ps := httprouter.Params{}
|
||||
|
|
@ -171,29 +181,22 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
defer rs.Body.Close()
|
||||
|
||||
By("verifying the HTTP response status code")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusOK), "Expected HTTP status 200 OK")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusOK))
|
||||
|
||||
By("reading the HTTP response body")
|
||||
body, err := io.ReadAll(rs.Body)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to read HTTP response body")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("unmarshalling the response JSON")
|
||||
By("unmarshalling the response JSON to WorkspacesEnvelope")
|
||||
var response WorkspacesEnvelope
|
||||
err = json.Unmarshal(body, &response)
|
||||
Expect(err).NotTo(HaveOccurred(), "Error unmarshalling response JSON")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("checking if 'workspaces' key exists in the response")
|
||||
workspacesData := response.Data
|
||||
By("getting the WorkspaceKind from the Kubernetes API")
|
||||
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
Expect(k8sClient.Get(ctx, workspaceKindKey, workspaceKind)).To(Succeed())
|
||||
|
||||
By("converting workspacesData to JSON and back to []WorkspaceModel")
|
||||
workspacesJSON, err := json.Marshal(workspacesData)
|
||||
Expect(err).NotTo(HaveOccurred(), "Error marshaling 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")
|
||||
By("getting the Workspaces from the Kubernetes API")
|
||||
workspace1 := &kubefloworgv1beta1.Workspace{}
|
||||
Expect(k8sClient.Get(ctx, workspaceKey1, workspace1)).To(Succeed())
|
||||
workspace2 := &kubefloworgv1beta1.Workspace{}
|
||||
|
|
@ -201,31 +204,30 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
workspace3 := &kubefloworgv1beta1.Workspace{}
|
||||
Expect(k8sClient.Get(ctx, workspaceKey3, workspace3)).To(Succeed())
|
||||
|
||||
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: workspaceKindName}, workspaceKind)).To(Succeed())
|
||||
|
||||
expectedWorkspaces := []models.WorkspaceModel{
|
||||
repositories.NewWorkspaceModelFromWorkspace(workspace1, workspaceKind),
|
||||
repositories.NewWorkspaceModelFromWorkspace(workspace2, workspaceKind),
|
||||
repositories.NewWorkspaceModelFromWorkspace(workspace3, workspaceKind),
|
||||
}
|
||||
Expect(workspaces).To(ConsistOf(expectedWorkspaces))
|
||||
By("ensuring the response contains the expected Workspaces")
|
||||
Expect(response.Data).To(ConsistOf(
|
||||
models.NewWorkspaceModelFromWorkspace(workspace1, workspaceKind),
|
||||
models.NewWorkspaceModelFromWorkspace(workspace2, workspaceKind),
|
||||
models.NewWorkspaceModelFromWorkspace(workspace3, workspaceKind),
|
||||
))
|
||||
|
||||
By("ensuring the response can be marshaled to JSON and back to []Workspace")
|
||||
dataJSON, err := json.Marshal(response.Data)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to marshal data to JSON")
|
||||
var dataObject []models.Workspace
|
||||
err = json.Unmarshal(dataJSON, &dataObject)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to unmarshal JSON to []Workspace")
|
||||
})
|
||||
|
||||
It("should retrieve the workspaces from namespaceName1 successfully", func() {
|
||||
|
||||
It("should retrieve Workspaces from Namespace 1 successfully", func() {
|
||||
By("creating the HTTP request")
|
||||
path := strings.Replace(WorkspacesByNamespacePath, ":"+NamespacePathParam, namespaceName1, 1)
|
||||
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("executing GetWorkspacesHandler")
|
||||
ps := httprouter.Params{
|
||||
httprouter.Param{
|
||||
Key: NamespacePathParam,
|
||||
Value: namespaceName1,
|
||||
},
|
||||
httprouter.Param{Key: NamespacePathParam, Value: namespaceName1},
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
a.GetWorkspacesHandler(rr, req, ps)
|
||||
|
|
@ -233,46 +235,313 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
defer rs.Body.Close()
|
||||
|
||||
By("verifying the HTTP response status code")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusOK), "Expected HTTP status 200 OK")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusOK))
|
||||
|
||||
By("reading the HTTP response body")
|
||||
body, err := io.ReadAll(rs.Body)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to read HTTP response body")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("unmarshalling the response JSON")
|
||||
By("unmarshalling the response JSON to WorkspacesEnvelope")
|
||||
var response WorkspacesEnvelope
|
||||
err = json.Unmarshal(body, &response)
|
||||
Expect(err).NotTo(HaveOccurred(), "Error unmarshalling response JSON")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("converting workspaces Data to JSON and back to []WorkspaceModel")
|
||||
workspacesJSON, err := json.Marshal(response.Data)
|
||||
Expect(err).NotTo(HaveOccurred(), "Error marshaling workspaces repositories")
|
||||
By("getting the WorkspaceKind from the Kubernetes API")
|
||||
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
Expect(k8sClient.Get(ctx, workspaceKindKey, workspaceKind)).To(Succeed())
|
||||
|
||||
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")
|
||||
By("getting the Workspaces from the Kubernetes API")
|
||||
workspace1 := &kubefloworgv1beta1.Workspace{}
|
||||
Expect(k8sClient.Get(ctx, workspaceKey1, workspace1)).To(Succeed())
|
||||
workspace2 := &kubefloworgv1beta1.Workspace{}
|
||||
Expect(k8sClient.Get(ctx, workspaceKey2, workspace2)).To(Succeed())
|
||||
|
||||
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: workspaceKindName}, workspaceKind)).To(Succeed())
|
||||
|
||||
expectedWorkspaces := []models.WorkspaceModel{
|
||||
repositories.NewWorkspaceModelFromWorkspace(workspace1, workspaceKind),
|
||||
repositories.NewWorkspaceModelFromWorkspace(workspace2, workspaceKind),
|
||||
}
|
||||
Expect(workspaces).To(ConsistOf(expectedWorkspaces))
|
||||
By("ensuring the response contains the expected Workspaces")
|
||||
Expect(response.Data).To(ConsistOf(
|
||||
models.NewWorkspaceModelFromWorkspace(workspace1, workspaceKind),
|
||||
models.NewWorkspaceModelFromWorkspace(workspace2, workspaceKind),
|
||||
))
|
||||
|
||||
By("ensuring the response can be marshaled to JSON and back to []Workspace")
|
||||
dataJSON, err := json.Marshal(response.Data)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to marshal data to JSON")
|
||||
var dataObject []models.Workspace
|
||||
err = json.Unmarshal(dataJSON, &dataObject)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to unmarshal JSON to []Workspace")
|
||||
})
|
||||
|
||||
It("should retrieve a single Workspace successfully", func() {
|
||||
By("creating the HTTP request")
|
||||
path := strings.Replace(WorkspacesByNamePath, ":"+NamespacePathParam, namespaceName1, 1)
|
||||
path = strings.Replace(path, ":"+WorkspaceNamePathParam, workspaceName1, 1)
|
||||
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("executing GetWorkspaceHandler")
|
||||
ps := httprouter.Params{
|
||||
httprouter.Param{Key: NamespacePathParam, Value: namespaceName1},
|
||||
httprouter.Param{Key: WorkspaceNamePathParam, Value: workspaceName1},
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
a.GetWorkspaceHandler(rr, req, ps)
|
||||
rs := rr.Result()
|
||||
defer rs.Body.Close()
|
||||
|
||||
By("verifying the HTTP response status code")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusOK))
|
||||
|
||||
By("reading the HTTP response body")
|
||||
body, err := io.ReadAll(rs.Body)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("unmarshalling the response JSON to WorkspaceEnvelope")
|
||||
var response WorkspaceEnvelope
|
||||
err = json.Unmarshal(body, &response)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("getting the WorkspaceKind from the Kubernetes API")
|
||||
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
Expect(k8sClient.Get(ctx, workspaceKindKey, workspaceKind)).To(Succeed())
|
||||
|
||||
By("getting the Workspace from the Kubernetes API")
|
||||
workspace := &kubefloworgv1beta1.Workspace{}
|
||||
Expect(k8sClient.Get(ctx, workspaceKey1, workspace)).To(Succeed())
|
||||
|
||||
By("ensuring the response matches the expected Workspace")
|
||||
Expect(response.Data).To(BeComparableTo(models.NewWorkspaceModelFromWorkspace(workspace, workspaceKind)))
|
||||
|
||||
By("ensuring the response can be marshaled to JSON and back to Workspace")
|
||||
dataJSON, err := json.Marshal(response.Data)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to marshal data to JSON")
|
||||
var dataObject models.Workspace
|
||||
err = json.Unmarshal(dataJSON, &dataObject)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to unmarshal JSON to Workspace")
|
||||
})
|
||||
})
|
||||
|
||||
Context("when there are no workspaces", func() {
|
||||
const otherNamespace = "otherNamespace"
|
||||
// NOTE: the tests in this context work on the same resources, they must be run in order.
|
||||
// also, they assume a specific state of the cluster, so cannot be run in parallel with other tests.
|
||||
// therefore, we run them using the `Ordered` and `Serial` Ginkgo decorators.
|
||||
Context("with existing invalid Workspaces", Serial, Ordered, func() {
|
||||
|
||||
const namespaceName1 = "ws-invalid-ns1"
|
||||
|
||||
var (
|
||||
a App
|
||||
|
||||
workspaceMissingWskName string
|
||||
workspaceMissingWskKey types.NamespacedName
|
||||
|
||||
workspaceInvalidPodConfig string
|
||||
workspaceInvalidPodConfigKey types.NamespacedName
|
||||
|
||||
workspaceInvalidImageConfig string
|
||||
workspaceInvalidImageConfigKey types.NamespacedName
|
||||
|
||||
workspaceKindName string
|
||||
workspaceKindKey types.NamespacedName
|
||||
)
|
||||
|
||||
BeforeAll(func() {
|
||||
uniqueName := "ws-invalid-test"
|
||||
workspaceMissingWskName = fmt.Sprintf("workspace-mising-wsk-%s", uniqueName)
|
||||
workspaceMissingWskKey = types.NamespacedName{Name: workspaceMissingWskName, Namespace: namespaceName1}
|
||||
workspaceInvalidPodConfig = fmt.Sprintf("workspace-invalid-pc-%s", uniqueName)
|
||||
workspaceInvalidPodConfigKey = types.NamespacedName{Name: workspaceInvalidPodConfig, Namespace: namespaceName1}
|
||||
workspaceInvalidImageConfig = fmt.Sprintf("workspace-invalid-ic-%s", uniqueName)
|
||||
workspaceInvalidImageConfigKey = types.NamespacedName{Name: workspaceInvalidImageConfig, Namespace: namespaceName1}
|
||||
workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName)
|
||||
workspaceKindKey = types.NamespacedName{Name: workspaceKindName}
|
||||
|
||||
repos := repositories.NewRepositories(k8sClient)
|
||||
a = App{
|
||||
Config: config.EnvConfig{
|
||||
Port: 4000,
|
||||
},
|
||||
repositories: repos,
|
||||
}
|
||||
|
||||
By("creating Namespace 1")
|
||||
namespace1 := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespaceName1,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, namespace1)).To(Succeed())
|
||||
|
||||
By("creating a WorkspaceKind")
|
||||
workspaceKind := NewExampleWorkspaceKind(workspaceKindName)
|
||||
Expect(k8sClient.Create(ctx, workspaceKind)).To(Succeed())
|
||||
|
||||
By("creating Workspace with missing WorkspaceKind")
|
||||
workspaceMissingWsk := NewExampleWorkspace(workspaceMissingWskName, namespaceName1, "bad-wsk")
|
||||
Expect(k8sClient.Create(ctx, workspaceMissingWsk)).To(Succeed())
|
||||
|
||||
By("creating Workspace with invalid PodConfig")
|
||||
workspaceInvalidPodConfig := NewExampleWorkspace(workspaceInvalidPodConfig, namespaceName1, workspaceKindName)
|
||||
workspaceInvalidPodConfig.Spec.PodTemplate.Options.PodConfig = "bad-pc"
|
||||
Expect(k8sClient.Create(ctx, workspaceInvalidPodConfig)).To(Succeed())
|
||||
|
||||
By("creating Workspace with invalid ImageConfig")
|
||||
workspaceInvalidImageConfig := NewExampleWorkspace(workspaceInvalidImageConfig, namespaceName1, workspaceKindName)
|
||||
workspaceInvalidImageConfig.Spec.PodTemplate.Options.ImageConfig = "bad-ic"
|
||||
Expect(k8sClient.Create(ctx, workspaceInvalidImageConfig)).To(Succeed())
|
||||
})
|
||||
|
||||
AfterAll(func() {
|
||||
By("deleting Workspace with missing WorkspaceKind")
|
||||
workspaceMissingWsk := &kubefloworgv1beta1.Workspace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: workspaceMissingWskName,
|
||||
Namespace: namespaceName1,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Delete(ctx, workspaceMissingWsk)).To(Succeed())
|
||||
|
||||
By("deleting Workspace with invalid PodConfig")
|
||||
workspaceInvalidPodConfig := &kubefloworgv1beta1.Workspace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: workspaceInvalidPodConfig,
|
||||
Namespace: namespaceName1,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Delete(ctx, workspaceInvalidPodConfig)).To(Succeed())
|
||||
|
||||
By("deleting Workspace with invalid ImageConfig")
|
||||
workspaceInvalidImageConfig := &kubefloworgv1beta1.Workspace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: workspaceInvalidImageConfig,
|
||||
Namespace: namespaceName1,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Delete(ctx, workspaceInvalidImageConfig)).To(Succeed())
|
||||
|
||||
By("deleting WorkspaceKind")
|
||||
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: workspaceKindName,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Delete(ctx, workspaceKind)).To(Succeed())
|
||||
|
||||
By("deleting Namespace 1")
|
||||
namespace1 := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespaceName1,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Delete(ctx, namespace1)).To(Succeed())
|
||||
})
|
||||
|
||||
It("should retrieve invalid Workspaces from Namespace 1 successfully", func() {
|
||||
By("creating the HTTP request")
|
||||
path := strings.Replace(WorkspacesByNamespacePath, ":"+NamespacePathParam, namespaceName1, 1)
|
||||
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("executing GetWorkspacesHandler")
|
||||
ps := httprouter.Params{
|
||||
httprouter.Param{Key: NamespacePathParam, Value: namespaceName1},
|
||||
}
|
||||
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))
|
||||
|
||||
By("reading the HTTP response body")
|
||||
body, err := io.ReadAll(rs.Body)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("unmarshalling the response JSON to WorkspacesEnvelope")
|
||||
var response WorkspacesEnvelope
|
||||
err = json.Unmarshal(body, &response)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("getting the WorkspaceKind from the Kubernetes API")
|
||||
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
Expect(k8sClient.Get(ctx, workspaceKindKey, workspaceKind)).To(Succeed())
|
||||
|
||||
By("getting the Workspaces from the Kubernetes API")
|
||||
workspaceMissingWsk := &kubefloworgv1beta1.Workspace{}
|
||||
Expect(k8sClient.Get(ctx, workspaceMissingWskKey, workspaceMissingWsk)).To(Succeed())
|
||||
workspaceInvalidPodConfig := &kubefloworgv1beta1.Workspace{}
|
||||
Expect(k8sClient.Get(ctx, workspaceInvalidPodConfigKey, workspaceInvalidPodConfig)).To(Succeed())
|
||||
workspaceInvalidImageConfig := &kubefloworgv1beta1.Workspace{}
|
||||
Expect(k8sClient.Get(ctx, workspaceInvalidImageConfigKey, workspaceInvalidImageConfig)).To(Succeed())
|
||||
|
||||
By("ensuring the model for Workspace with missing WorkspaceKind is as expected")
|
||||
workspaceMissingWskModel := models.NewWorkspaceModelFromWorkspace(workspaceMissingWsk, nil)
|
||||
Expect(workspaceMissingWskModel.WorkspaceKind.Missing).To(BeTrue())
|
||||
Expect(workspaceMissingWskModel.PodTemplate.Volumes.Home.MountPath).To(Equal(models.UnknownHomeMountPath))
|
||||
Expect(workspaceMissingWskModel.PodTemplate.Options.PodConfig.Current.DisplayName).To(Equal(models.UnknownPodConfig))
|
||||
Expect(workspaceMissingWskModel.PodTemplate.Options.PodConfig.Current.Description).To(Equal(models.UnknownPodConfig))
|
||||
Expect(workspaceMissingWskModel.PodTemplate.Options.ImageConfig.Current.DisplayName).To(Equal(models.UnknownImageConfig))
|
||||
Expect(workspaceMissingWskModel.PodTemplate.Options.ImageConfig.Current.Description).To(Equal(models.UnknownImageConfig))
|
||||
|
||||
By("ensuring the model for Workspace with invalid PodConfig is as expected")
|
||||
workspaceInvalidPodConfigModel := models.NewWorkspaceModelFromWorkspace(workspaceInvalidPodConfig, workspaceKind)
|
||||
Expect(workspaceInvalidPodConfigModel.PodTemplate.Options.PodConfig.Current.DisplayName).To(Equal(models.UnknownPodConfig))
|
||||
Expect(workspaceInvalidPodConfigModel.PodTemplate.Options.PodConfig.Current.Description).To(Equal(models.UnknownPodConfig))
|
||||
|
||||
By("ensuring the model for Workspace with invalid ImageConfig is as expected")
|
||||
workspaceInvalidImageConfigModel := models.NewWorkspaceModelFromWorkspace(workspaceInvalidImageConfig, workspaceKind)
|
||||
Expect(workspaceInvalidImageConfigModel.PodTemplate.Options.ImageConfig.Current.DisplayName).To(Equal(models.UnknownImageConfig))
|
||||
Expect(workspaceInvalidImageConfigModel.PodTemplate.Options.ImageConfig.Current.Description).To(Equal(models.UnknownImageConfig))
|
||||
|
||||
By("ensuring the response contains the expected Workspaces")
|
||||
Expect(response.Data).To(ConsistOf(
|
||||
workspaceMissingWskModel,
|
||||
workspaceInvalidPodConfigModel,
|
||||
workspaceInvalidImageConfigModel,
|
||||
))
|
||||
})
|
||||
|
||||
It("should retrieve a single invalid Workspace successfully", func() {
|
||||
By("creating the HTTP request")
|
||||
path := strings.Replace(WorkspacesByNamePath, ":"+NamespacePathParam, namespaceName1, 1)
|
||||
path = strings.Replace(path, ":"+WorkspaceNamePathParam, workspaceMissingWskName, 1)
|
||||
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("executing GetWorkspaceHandler")
|
||||
ps := httprouter.Params{
|
||||
httprouter.Param{Key: NamespacePathParam, Value: namespaceName1},
|
||||
httprouter.Param{Key: WorkspaceNamePathParam, Value: workspaceMissingWskName},
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
a.GetWorkspaceHandler(rr, req, ps)
|
||||
rs := rr.Result()
|
||||
defer rs.Body.Close()
|
||||
|
||||
By("verifying the HTTP response status code")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusOK))
|
||||
|
||||
By("reading the HTTP response body")
|
||||
body, err := io.ReadAll(rs.Body)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("unmarshalling the response JSON to WorkspaceEnvelope")
|
||||
var response WorkspaceEnvelope
|
||||
err = json.Unmarshal(body, &response)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("getting the Workspace from the Kubernetes API")
|
||||
workspaceMissingWsk := &kubefloworgv1beta1.Workspace{}
|
||||
Expect(k8sClient.Get(ctx, workspaceMissingWskKey, workspaceMissingWsk)).To(Succeed())
|
||||
|
||||
By("ensuring the response matches the expected Workspace")
|
||||
workspaceMissingWskModel := models.NewWorkspaceModelFromWorkspace(workspaceMissingWsk, nil)
|
||||
Expect(response.Data).To(BeComparableTo(workspaceMissingWskModel))
|
||||
})
|
||||
})
|
||||
|
||||
// NOTE: these tests assume a specific state of the cluster, so cannot be run in parallel with other tests.
|
||||
// therefore, we run them using the `Serial` Ginkgo decorators.
|
||||
Context("with no existing Workspaces", Serial, func() {
|
||||
|
||||
var a App
|
||||
|
||||
BeforeEach(func() {
|
||||
|
|
@ -284,18 +553,46 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
repositories: repos,
|
||||
}
|
||||
})
|
||||
It("should return an empty list of workspaces", func() {
|
||||
|
||||
It("should return an empty list of Workspaces for all namespaces", func() {
|
||||
By("creating the HTTP request")
|
||||
path := strings.Replace(AllWorkspacesPath, ":"+NamespacePathParam, otherNamespace, 1)
|
||||
req, err := http.NewRequest(http.MethodGet, AllWorkspacesPath, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
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))
|
||||
|
||||
By("reading the HTTP response body")
|
||||
body, err := io.ReadAll(rs.Body)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("unmarshalling the response JSON to WorkspacesEnvelope")
|
||||
var response WorkspacesEnvelope
|
||||
err = json.Unmarshal(body, &response)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("ensuring that no Workspaces were returned")
|
||||
Expect(response.Data).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("should return an empty list of Workspaces for a non-existent namespace", func() {
|
||||
missingNamespace := "non-existent-namespace"
|
||||
|
||||
By("creating the HTTP request")
|
||||
path := strings.Replace(AllWorkspacesPath, ":"+NamespacePathParam, missingNamespace, 1)
|
||||
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("executing GetWorkspacesHandler")
|
||||
ps := httprouter.Params{
|
||||
httprouter.Param{
|
||||
Key: NamespacePathParam,
|
||||
Value: otherNamespace,
|
||||
},
|
||||
httprouter.Param{Key: NamespacePathParam, Value: missingNamespace},
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
a.GetWorkspacesHandler(rr, req, ps)
|
||||
|
|
@ -303,40 +600,66 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
defer rs.Body.Close()
|
||||
|
||||
By("verifying the HTTP response status code")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusOK), "Expected HTTP status 200 OK")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusOK))
|
||||
|
||||
By("reading the HTTP response body")
|
||||
body, err := io.ReadAll(rs.Body)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to read HTTP response body")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("unmarshalling the response JSON")
|
||||
By("unmarshalling the response JSON to WorkspacesEnvelope")
|
||||
var response WorkspacesEnvelope
|
||||
err = json.Unmarshal(body, &response)
|
||||
Expect(err).NotTo(HaveOccurred(), "Error unmarshalling response JSON")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("asserting that the 'workspaces' list is empty")
|
||||
workspacesJSON, err := json.Marshal(response.Data)
|
||||
Expect(err).NotTo(HaveOccurred(), "Error marshaling workspaces data")
|
||||
By("ensuring that no Workspaces were returned")
|
||||
Expect(response.Data).To(BeEmpty())
|
||||
})
|
||||
|
||||
var workspaces []models.WorkspaceModel
|
||||
err = json.Unmarshal(workspacesJSON, &workspaces)
|
||||
Expect(err).NotTo(HaveOccurred(), "Error unmarshalling workspaces JSON")
|
||||
Expect(workspaces).To(BeEmpty(), "Expected no workspaces in the response")
|
||||
It("should return 404 for a non-existent Workspace", func() {
|
||||
missingNamespace := "non-existent-namespace"
|
||||
missingWorkspaceName := "non-existent-workspace"
|
||||
|
||||
By("creating the HTTP request")
|
||||
path := strings.Replace(WorkspacesByNamePath, ":"+NamespacePathParam, missingNamespace, 1)
|
||||
path = strings.Replace(path, ":"+WorkspaceNamePathParam, missingWorkspaceName, 1)
|
||||
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("executing GetWorkspaceHandler")
|
||||
ps := httprouter.Params{
|
||||
httprouter.Param{Key: NamespacePathParam, Value: missingNamespace},
|
||||
httprouter.Param{Key: WorkspaceNamePathParam, Value: missingWorkspaceName},
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
a.GetWorkspaceHandler(rr, req, ps)
|
||||
rs := rr.Result()
|
||||
defer rs.Body.Close()
|
||||
|
||||
By("verifying the HTTP response status code")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusNotFound))
|
||||
})
|
||||
})
|
||||
|
||||
Context("CRUD workspace", Ordered, func() {
|
||||
// NOTE: the tests in this context work on the same resources, they must be run in order.
|
||||
// therefore, we run them using the `Ordered` Ginkgo decorator.
|
||||
Context("CRUD Workspaces", Ordered, func() {
|
||||
|
||||
const namespaceNameCrud = "namespace-crud"
|
||||
const namespaceNameCrud = "ws-crud-ns"
|
||||
|
||||
var (
|
||||
a App
|
||||
workspaceName string
|
||||
workspaceKey types.NamespacedName
|
||||
workspaceKindName string
|
||||
workspaceKindKey types.NamespacedName
|
||||
)
|
||||
|
||||
BeforeAll(func() {
|
||||
uniqueName := "wsk-update-test"
|
||||
uniqueName := "ws-crud-test"
|
||||
workspaceName = fmt.Sprintf("workspace-%s", uniqueName)
|
||||
workspaceKey = types.NamespacedName{Name: workspaceName, Namespace: namespaceNameCrud}
|
||||
workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName)
|
||||
workspaceKindKey = types.NamespacedName{Name: workspaceKindName}
|
||||
|
||||
repos := repositories.NewRepositories(k8sClient)
|
||||
a = App{
|
||||
|
|
@ -346,7 +669,7 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
repositories: repos,
|
||||
}
|
||||
|
||||
By("creating namespace")
|
||||
By("creating the Namespace")
|
||||
namespaceA := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespaceNameCrud,
|
||||
|
|
@ -354,14 +677,13 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
}
|
||||
Expect(k8sClient.Create(ctx, namespaceA)).To(Succeed())
|
||||
|
||||
By("creating a WorkspaceKind")
|
||||
By("creating the WorkspaceKind")
|
||||
workspaceKind := NewExampleWorkspaceKind(workspaceKindName)
|
||||
Expect(k8sClient.Create(ctx, workspaceKind)).To(Succeed())
|
||||
|
||||
})
|
||||
|
||||
AfterAll(func() {
|
||||
|
||||
By("deleting the WorkspaceKind")
|
||||
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
|
@ -370,86 +692,70 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
}
|
||||
Expect(k8sClient.Delete(ctx, workspaceKind)).To(Succeed())
|
||||
|
||||
By("deleting the namespace")
|
||||
By("deleting the Namespace")
|
||||
namespaceA := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespaceNameCrud,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Delete(ctx, namespaceA)).To(Succeed())
|
||||
|
||||
})
|
||||
|
||||
It("should create, retrieve and delete workspace successfully", func() {
|
||||
It("should create and delete a Workspace successfully", func() {
|
||||
|
||||
By("creating the workspace via the API")
|
||||
workspaceName := "dora"
|
||||
workspaceModel := models.WorkspaceModel{
|
||||
Name: workspaceName,
|
||||
Namespace: namespaceNameCrud,
|
||||
WorkspaceKind: models.WorkspaceKind{
|
||||
Name: workspaceKindName,
|
||||
Type: "POD_TEMPLATE",
|
||||
By("getting the WorkspaceKind from the Kubernetes API")
|
||||
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
Expect(k8sClient.Get(ctx, workspaceKindKey, workspaceKind)).To(Succeed())
|
||||
|
||||
By("defining the Workspace to create")
|
||||
workspace := &kubefloworgv1beta1.Workspace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: workspaceName,
|
||||
Namespace: namespaceNameCrud,
|
||||
},
|
||||
DeferUpdates: false,
|
||||
Paused: false,
|
||||
PausedTime: 0,
|
||||
State: "",
|
||||
StateMessage: "",
|
||||
PodTemplate: models.PodTemplate{
|
||||
PodMetadata: &models.PodMetadata{
|
||||
Labels: map[string]string{
|
||||
"app": "dora",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"app": "dora",
|
||||
},
|
||||
},
|
||||
Volumes: &models.Volumes{
|
||||
Home: &models.DataVolumeModel{
|
||||
PvcName: "my-data-pvc",
|
||||
MountPath: "/home/jovyan",
|
||||
ReadOnly: false,
|
||||
},
|
||||
Data: []models.DataVolumeModel{
|
||||
{
|
||||
PvcName: "my-data-pvc",
|
||||
MountPath: "/data",
|
||||
ReadOnly: false,
|
||||
Spec: kubefloworgv1beta1.WorkspaceSpec{
|
||||
Kind: workspaceKindName,
|
||||
Paused: ptr.To(false),
|
||||
DeferUpdates: ptr.To(false),
|
||||
PodTemplate: kubefloworgv1beta1.WorkspacePodTemplate{
|
||||
PodMetadata: &kubefloworgv1beta1.WorkspacePodMetadata{
|
||||
Labels: map[string]string{
|
||||
"app": "dora",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"app": "dora",
|
||||
},
|
||||
},
|
||||
},
|
||||
ImageConfig: &models.ImageConfig{
|
||||
Current: "WorkspaceKind",
|
||||
Desired: "", // Status is coming with empty value
|
||||
RedirectChain: []*models.RedirectChain{},
|
||||
},
|
||||
PodConfig: &models.PodConfig{
|
||||
Current: "WorkspaceKind",
|
||||
Desired: "WorkspaceKind",
|
||||
RedirectChain: []*models.RedirectChain{},
|
||||
},
|
||||
},
|
||||
Activity: models.Activity{
|
||||
LastActivity: 0,
|
||||
LastUpdate: 0,
|
||||
LastProbe: &models.Probe{
|
||||
StartTimeMs: 0,
|
||||
EndTimeMs: 0,
|
||||
Result: "default_result",
|
||||
Message: "default_message",
|
||||
Volumes: kubefloworgv1beta1.WorkspacePodVolumes{
|
||||
Home: ptr.To("my-home-pvc"),
|
||||
Data: []kubefloworgv1beta1.PodVolumeMount{
|
||||
{
|
||||
PVCName: "my-data-pvc",
|
||||
MountPath: "/data",
|
||||
ReadOnly: ptr.To(false),
|
||||
},
|
||||
},
|
||||
},
|
||||
Options: kubefloworgv1beta1.WorkspacePodOptions{
|
||||
ImageConfig: "jupyterlab_scipy_180",
|
||||
PodConfig: "tiny_cpu",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
workspaceModel := models.NewWorkspaceModelFromWorkspace(workspace, workspaceKind)
|
||||
|
||||
workspaceJSON, err := json.Marshal(workspaceModel)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to marshal WorkspaceModel to JSON")
|
||||
By("marshaling the Workspace model to JSON")
|
||||
workspaceModelJSON, err := json.Marshal(workspaceModel)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("creating an HTTP request to create the Workspace")
|
||||
path := strings.Replace(WorkspacesByNamespacePath, ":"+NamespacePathParam, namespaceNameCrud, 1)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, path, strings.NewReader(string(workspaceJSON)))
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")
|
||||
req, err := http.NewRequest(http.MethodPost, path, strings.NewReader(string(workspaceModelJSON)))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
By("executing CreateWorkspaceHandler")
|
||||
rr := httptest.NewRecorder()
|
||||
ps := httprouter.Params{
|
||||
httprouter.Param{
|
||||
|
|
@ -457,18 +763,28 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
Value: namespaceNameCrud,
|
||||
},
|
||||
}
|
||||
|
||||
a.CreateWorkspaceHandler(rr, req, ps)
|
||||
rs := rr.Result()
|
||||
defer rs.Body.Close()
|
||||
|
||||
By("verifying the HTTP response status code for creation")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusCreated), "Expected HTTP status 201 Created")
|
||||
By("verifying the HTTP response status code")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusCreated))
|
||||
|
||||
By("retrieving the created workspace via the API")
|
||||
By("getting the created Workspace from the Kubernetes API")
|
||||
createdWorkspace := &kubefloworgv1beta1.Workspace{}
|
||||
Expect(k8sClient.Get(ctx, workspaceKey, createdWorkspace)).To(Succeed())
|
||||
|
||||
By("ensuring the created Workspace matches the expected Workspace")
|
||||
Expect(createdWorkspace.Spec).To(BeComparableTo(workspace.Spec))
|
||||
|
||||
By("creating an HTTP request to delete the Workspace")
|
||||
path = strings.Replace(WorkspacesByNamePath, ":"+NamespacePathParam, namespaceNameCrud, 1)
|
||||
path = strings.Replace(path, ":"+WorkspaceNamePathParam, workspaceName, 1)
|
||||
req, err = http.NewRequest(http.MethodDelete, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("executing DeleteWorkspaceHandler")
|
||||
rr = httptest.NewRecorder()
|
||||
ps = httprouter.Params{
|
||||
httprouter.Param{
|
||||
Key: NamespacePathParam,
|
||||
|
|
@ -479,69 +795,18 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
Value: workspaceName,
|
||||
},
|
||||
}
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")
|
||||
rr = httptest.NewRecorder()
|
||||
|
||||
a.GetWorkspaceHandler(rr, req, ps)
|
||||
rs = rr.Result()
|
||||
defer rs.Body.Close()
|
||||
|
||||
By("verifying the HTTP response status code for retrieval")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusOK), "Expected HTTP status 200 OK")
|
||||
|
||||
By("reading the HTTP response body for retrieval")
|
||||
body, err := io.ReadAll(rs.Body)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to read HTTP response body")
|
||||
|
||||
By("unmarshalling the response JSON for retrieval")
|
||||
var response WorkspaceEnvelope
|
||||
|
||||
err = json.Unmarshal(body, &response)
|
||||
Expect(err).NotTo(HaveOccurred(), "Error unmarshalling response JSON")
|
||||
|
||||
response.Data.Activity.LastActivity = 0
|
||||
|
||||
By("checking if the retrieved workspace matches the expected workspace")
|
||||
retrievedWorkspaceJSON, err := json.Marshal(response.Data)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to marshal retrieved workspace to JSON")
|
||||
|
||||
originalWorkspaceJSON, err := json.Marshal(workspaceModel)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to marshal original workspace to JSON")
|
||||
|
||||
Expect(retrievedWorkspaceJSON).To(MatchJSON(originalWorkspaceJSON), "The retrieved workspace does not match the created one")
|
||||
|
||||
By("deleting the workspace via the API")
|
||||
req, err = http.NewRequest(http.MethodDelete, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request for deletion")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
a.DeleteWorkspaceHandler(rr, req, ps)
|
||||
rs = rr.Result()
|
||||
defer rs.Body.Close()
|
||||
|
||||
By("verifying the HTTP response status code for deletion")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusNoContent), "Expected HTTP status 204 No Content")
|
||||
|
||||
By("verifying the workspace has been deleted")
|
||||
req, err = http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")
|
||||
rr = httptest.NewRecorder()
|
||||
|
||||
a.GetWorkspaceHandler(rr, req, ps)
|
||||
rs = rr.Result()
|
||||
defer rs.Body.Close()
|
||||
|
||||
By("verifying the HTTP response status code for not found")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusNotFound), "Expected HTTP status 200 OK")
|
||||
|
||||
By("double check via k9client")
|
||||
workspace := &kubefloworgv1beta1.Workspace{}
|
||||
err = k8sClient.Get(ctx, types.NamespacedName{Name: "dora", Namespace: namespaceNameCrud}, workspace)
|
||||
Expect(err).To(HaveOccurred(), "Expected error when retrieving the deleted workspace")
|
||||
Expect(err).To(MatchError(`workspaces.kubeflow.org "dora" not found`))
|
||||
By("verifying the HTTP response status code")
|
||||
Expect(rs.StatusCode).To(Equal(http.StatusNoContent))
|
||||
|
||||
By("ensuring the Workspace has been deleted")
|
||||
deletedWorkspace := &kubefloworgv1beta1.Workspace{}
|
||||
err = k8sClient.Get(ctx, workspaceKey, deletedWorkspace)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(apierrors.IsNotFound(err)).To(BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,13 +14,20 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package models
|
||||
package health_check
|
||||
|
||||
type HealthCheck struct {
|
||||
Status ServiceStatus `json:"status"`
|
||||
SystemInfo SystemInfo `json:"system_info"`
|
||||
}
|
||||
|
||||
type SystemInfo struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type HealthCheckModel struct {
|
||||
Status string `json:"status"`
|
||||
SystemInfo SystemInfo `json:"system_info"`
|
||||
}
|
||||
type ServiceStatus string
|
||||
|
||||
const (
|
||||
ServiceStatusHealthy ServiceStatus = "Healthy"
|
||||
ServiceStatusUnhealthy ServiceStatus = "Unhealthy"
|
||||
)
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
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 namespaces
|
||||
|
||||
import corev1 "k8s.io/api/core/v1"
|
||||
|
||||
// NewNamespaceModelFromNamespace creates a new Namespace model from a Namespace object.
|
||||
func NewNamespaceModelFromNamespace(ns *corev1.Namespace) Namespace {
|
||||
return Namespace{
|
||||
Name: ns.Name,
|
||||
}
|
||||
}
|
||||
|
|
@ -14,14 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package models
|
||||
package namespaces
|
||||
|
||||
type NamespaceModel struct {
|
||||
type Namespace struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func NewNamespaceModelFromNamespace(name string) NamespaceModel {
|
||||
return NamespaceModel{
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
/*
|
||||
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 models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
|
||||
)
|
||||
|
||||
type WorkspaceKindModel struct {
|
||||
Name string `json:"name"`
|
||||
PodTemplate PodTemplateModel `json:"pod_template"`
|
||||
Spawner SpawnerModel `json:"spawner"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
type PodTemplateModel struct {
|
||||
ImageConfig string `json:"image_config"`
|
||||
PodConfig string `json:"pod_config"`
|
||||
Resources ResourceModel `json:"resources"`
|
||||
}
|
||||
|
||||
type ResourceModel struct {
|
||||
Cpu string `json:"cpu"`
|
||||
Memory string `json:"memory"`
|
||||
}
|
||||
|
||||
type SpawnerModel struct {
|
||||
DisplayName string `json:"display_name"`
|
||||
Description string `json:"description"`
|
||||
Deprecated bool `json:"deprecated"`
|
||||
DeprecationMessage string `json:"deprecation_message"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
||||
func NewWorkspaceKindModelFromWorkspaceKind(item *kubefloworgv1beta1.WorkspaceKind) WorkspaceKindModel {
|
||||
deprecated := false
|
||||
if item.Spec.Spawner.Deprecated != nil {
|
||||
deprecated = *item.Spec.Spawner.Deprecated
|
||||
}
|
||||
|
||||
hidden := false
|
||||
if item.Spec.Spawner.Hidden != nil {
|
||||
hidden = *item.Spec.Spawner.Hidden
|
||||
}
|
||||
|
||||
deprecationMessage := ""
|
||||
if item.Spec.Spawner.DeprecationMessage != nil {
|
||||
deprecationMessage = *item.Spec.Spawner.DeprecationMessage
|
||||
}
|
||||
|
||||
cpuValues := make([]string, len(item.Spec.PodTemplate.Options.PodConfig.Values))
|
||||
memoryValues := make([]string, len(item.Spec.PodTemplate.Options.PodConfig.Values))
|
||||
for i, value := range item.Spec.PodTemplate.Options.PodConfig.Values {
|
||||
cpuValues[i] = value.Spec.Resources.Requests.Cpu().String()
|
||||
memoryValues[i] = value.Spec.Resources.Requests.Memory().String()
|
||||
}
|
||||
|
||||
workspaceKindModel := WorkspaceKindModel{
|
||||
Name: item.Name,
|
||||
Labels: item.Labels,
|
||||
Annotations: item.Annotations,
|
||||
Spawner: SpawnerModel{
|
||||
DisplayName: item.Spec.Spawner.DisplayName,
|
||||
Description: item.Spec.Spawner.Description,
|
||||
Deprecated: deprecated,
|
||||
DeprecationMessage: deprecationMessage,
|
||||
Hidden: hidden,
|
||||
},
|
||||
PodTemplate: PodTemplateModel{
|
||||
ImageConfig: item.Spec.PodTemplate.Options.ImageConfig.Spawner.Default,
|
||||
PodConfig: strings.Join(cpuValues, ",") + "|" + strings.Join(memoryValues, ","),
|
||||
Resources: ResourceModel{
|
||||
Cpu: strings.Join(cpuValues, ", "),
|
||||
Memory: strings.Join(memoryValues, ", "),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return workspaceKindModel
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
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 workspacekinds
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
// NewWorkspaceKindModelFromWorkspaceKind creates a WorkspaceKind model from a WorkspaceKind object.
|
||||
func NewWorkspaceKindModelFromWorkspaceKind(wsk *kubefloworgv1beta1.WorkspaceKind) WorkspaceKind {
|
||||
podLabels := make(map[string]string)
|
||||
podAnnotations := make(map[string]string)
|
||||
if wsk.Spec.PodTemplate.PodMetadata != nil {
|
||||
// NOTE: we copy the maps to avoid creating a reference to the original maps.
|
||||
for k, v := range wsk.Spec.PodTemplate.PodMetadata.Labels {
|
||||
podLabels[k] = v
|
||||
}
|
||||
for k, v := range wsk.Spec.PodTemplate.PodMetadata.Annotations {
|
||||
podAnnotations[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: icons can either be a remote URL or read from a ConfigMap.
|
||||
// in BOTH cases, we should cache and serve the image under a path on the backend API:
|
||||
// /api/v1/workspacekinds/{name}/assets/icon
|
||||
iconRef := ImageRef{
|
||||
URL: fmt.Sprintf("/workspaces/backend/api/v1/workspacekinds/%s/assets/icon", wsk.Name),
|
||||
}
|
||||
|
||||
// TODO: logos can either be a remote URL or read from a ConfigMap.
|
||||
// in BOTH cases, we should cache and serve the image under a path on the backend API:
|
||||
// /api/v1/workspacekinds/{name}/assets/logo
|
||||
logoRef := ImageRef{
|
||||
URL: fmt.Sprintf("/workspaces/backend/api/v1/workspacekinds/%s/assets/logo", wsk.Name),
|
||||
}
|
||||
|
||||
return WorkspaceKind{
|
||||
Name: wsk.Name,
|
||||
DisplayName: wsk.Spec.Spawner.DisplayName,
|
||||
Description: wsk.Spec.Spawner.Description,
|
||||
Deprecated: ptr.Deref(wsk.Spec.Spawner.Deprecated, false),
|
||||
DeprecationMessage: ptr.Deref(wsk.Spec.Spawner.DeprecationMessage, ""),
|
||||
Hidden: ptr.Deref(wsk.Spec.Spawner.Hidden, false),
|
||||
Icon: iconRef,
|
||||
Logo: logoRef,
|
||||
PodTemplate: PodTemplate{
|
||||
PodMetadata: PodMetadata{
|
||||
Labels: podLabels,
|
||||
Annotations: podAnnotations,
|
||||
},
|
||||
VolumeMounts: PodVolumeMounts{
|
||||
Home: wsk.Spec.PodTemplate.VolumeMounts.Home,
|
||||
},
|
||||
Options: PodTemplateOptions{
|
||||
ImageConfig: ImageConfig{
|
||||
Default: wsk.Spec.PodTemplate.Options.ImageConfig.Spawner.Default,
|
||||
Values: buildImageConfigValues(wsk.Spec.PodTemplate.Options.ImageConfig),
|
||||
},
|
||||
PodConfig: PodConfig{
|
||||
Default: wsk.Spec.PodTemplate.Options.PodConfig.Spawner.Default,
|
||||
Values: buildPodConfigValues(wsk.Spec.PodTemplate.Options.PodConfig),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildImageConfigValues(imageConfig kubefloworgv1beta1.ImageConfig) []ImageConfigValue {
|
||||
imageConfigValues := make([]ImageConfigValue, len(imageConfig.Values))
|
||||
for i := range imageConfig.Values {
|
||||
option := imageConfig.Values[i]
|
||||
imageConfigValues[i] = ImageConfigValue{
|
||||
Id: option.Id,
|
||||
DisplayName: option.Spawner.DisplayName,
|
||||
Description: ptr.Deref(option.Spawner.Description, ""),
|
||||
Labels: buildOptionLabels(option.Spawner.Labels),
|
||||
Hidden: ptr.Deref(option.Spawner.Hidden, false),
|
||||
Redirect: buildOptionRedirect(option.Redirect),
|
||||
}
|
||||
}
|
||||
return imageConfigValues
|
||||
}
|
||||
|
||||
func buildPodConfigValues(podConfig kubefloworgv1beta1.PodConfig) []PodConfigValue {
|
||||
podConfigValues := make([]PodConfigValue, len(podConfig.Values))
|
||||
for i := range podConfig.Values {
|
||||
option := podConfig.Values[i]
|
||||
podConfigValues[i] = PodConfigValue{
|
||||
Id: option.Id,
|
||||
DisplayName: option.Spawner.DisplayName,
|
||||
Description: ptr.Deref(option.Spawner.Description, ""),
|
||||
Labels: buildOptionLabels(option.Spawner.Labels),
|
||||
Hidden: ptr.Deref(option.Spawner.Hidden, false),
|
||||
Redirect: buildOptionRedirect(option.Redirect),
|
||||
}
|
||||
}
|
||||
return podConfigValues
|
||||
}
|
||||
|
||||
func buildOptionLabels(labels []kubefloworgv1beta1.OptionSpawnerLabel) []OptionLabel {
|
||||
optionLabels := make([]OptionLabel, len(labels))
|
||||
for i := range labels {
|
||||
optionLabels[i] = OptionLabel{
|
||||
Key: labels[i].Key,
|
||||
Value: labels[i].Value,
|
||||
}
|
||||
}
|
||||
return optionLabels
|
||||
}
|
||||
|
||||
func buildOptionRedirect(redirect *kubefloworgv1beta1.OptionRedirect) *OptionRedirect {
|
||||
if redirect == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var message *RedirectMessage
|
||||
if redirect.Message != nil {
|
||||
messageLevel := RedirectMessageLevelInfo
|
||||
switch redirect.Message.Level {
|
||||
case kubefloworgv1beta1.RedirectMessageLevelInfo:
|
||||
messageLevel = RedirectMessageLevelInfo
|
||||
case kubefloworgv1beta1.RedirectMessageLevelWarning:
|
||||
messageLevel = RedirectMessageLevelWarning
|
||||
case kubefloworgv1beta1.RedirectMessageLevelDanger:
|
||||
messageLevel = RedirectMessageLevelDanger
|
||||
}
|
||||
|
||||
message = &RedirectMessage{
|
||||
Text: redirect.Message.Text,
|
||||
Level: messageLevel,
|
||||
}
|
||||
}
|
||||
|
||||
return &OptionRedirect{
|
||||
To: redirect.To,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
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 workspacekinds
|
||||
|
||||
type WorkspaceKind struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Description string `json:"description"`
|
||||
Deprecated bool `json:"deprecated"`
|
||||
DeprecationMessage string `json:"deprecation_message"`
|
||||
Hidden bool `json:"hidden"`
|
||||
Icon ImageRef `json:"icon"`
|
||||
Logo ImageRef `json:"logo"`
|
||||
PodTemplate PodTemplate `json:"pod_template"`
|
||||
}
|
||||
|
||||
type ImageRef struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type PodTemplate struct {
|
||||
PodMetadata PodMetadata `json:"pod_metadata"`
|
||||
VolumeMounts PodVolumeMounts `json:"volume_mounts"`
|
||||
Options PodTemplateOptions `json:"options"`
|
||||
}
|
||||
|
||||
type PodMetadata struct {
|
||||
Labels map[string]string `json:"labels"`
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
}
|
||||
|
||||
type PodVolumeMounts struct {
|
||||
Home string `json:"home"`
|
||||
}
|
||||
|
||||
type PodTemplateOptions struct {
|
||||
ImageConfig ImageConfig `json:"image_config"`
|
||||
PodConfig PodConfig `json:"pod_config"`
|
||||
}
|
||||
|
||||
type ImageConfig struct {
|
||||
Default string `json:"default"`
|
||||
Values []ImageConfigValue `json:"values"`
|
||||
}
|
||||
|
||||
type ImageConfigValue struct {
|
||||
Id string `json:"id"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Description string `json:"description"`
|
||||
Labels []OptionLabel `json:"labels"`
|
||||
Hidden bool `json:"hidden"`
|
||||
Redirect *OptionRedirect `json:"redirect,omitempty"`
|
||||
}
|
||||
|
||||
type PodConfig struct {
|
||||
Default string `json:"default"`
|
||||
Values []PodConfigValue `json:"values"`
|
||||
}
|
||||
|
||||
type PodConfigValue struct {
|
||||
Id string `json:"id"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Description string `json:"description"`
|
||||
Labels []OptionLabel `json:"labels"`
|
||||
Hidden bool `json:"hidden"`
|
||||
Redirect *OptionRedirect `json:"redirect,omitempty"`
|
||||
}
|
||||
|
||||
type OptionLabel struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type OptionRedirect struct {
|
||||
To string `json:"to"`
|
||||
Message *RedirectMessage `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type RedirectMessage struct {
|
||||
Text string `json:"text"`
|
||||
Level RedirectMessageLevel `json:"level"`
|
||||
}
|
||||
|
||||
type RedirectMessageLevel string
|
||||
|
||||
const (
|
||||
RedirectMessageLevelInfo RedirectMessageLevel = "Info"
|
||||
RedirectMessageLevelWarning RedirectMessageLevel = "Warning"
|
||||
RedirectMessageLevelDanger RedirectMessageLevel = "Danger"
|
||||
)
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
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 models
|
||||
|
||||
type WorkspaceModel struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
WorkspaceKind WorkspaceKind `json:"workspace_kind"`
|
||||
DeferUpdates bool `json:"defer_updates"`
|
||||
Paused bool `json:"paused"`
|
||||
PausedTime int64 `json:"paused_time"`
|
||||
State string `json:"state"`
|
||||
StateMessage string `json:"state_message"`
|
||||
PodTemplate PodTemplate `json:"pod_template"`
|
||||
Activity Activity `json:"activity"`
|
||||
}
|
||||
type PodTemplate struct {
|
||||
PodMetadata *PodMetadata `json:"pod_metadata"`
|
||||
Volumes *Volumes `json:"volumes"`
|
||||
ImageConfig *ImageConfig `json:"image_config"`
|
||||
PodConfig *PodConfig `json:"pod_config"`
|
||||
}
|
||||
|
||||
type PodMetadata struct {
|
||||
Labels map[string]string `json:"labels"`
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
}
|
||||
type Volumes struct {
|
||||
Home *DataVolumeModel `json:"home"`
|
||||
Data []DataVolumeModel `json:"data"`
|
||||
}
|
||||
|
||||
type ImageConfig struct {
|
||||
Current string `json:"current"`
|
||||
Desired string `json:"desired"`
|
||||
RedirectChain []*RedirectChain `json:"redirect_chain"`
|
||||
}
|
||||
|
||||
type PodConfig struct {
|
||||
Current string `json:"current"`
|
||||
Desired string `json:"desired"`
|
||||
RedirectChain []*RedirectChain `json:"redirect_chain"`
|
||||
}
|
||||
|
||||
type RedirectChain struct {
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
type Activity struct {
|
||||
LastActivity int64 `json:"last_activity"` // Unix Epoch time
|
||||
LastUpdate int64 `json:"last_update"` // Unix Epoch time
|
||||
LastProbe *Probe `json:"last_probe"`
|
||||
}
|
||||
|
||||
type Probe struct {
|
||||
StartTimeMs int64 `json:"start_time_ms"` // Unix Epoch time in milliseconds
|
||||
EndTimeMs int64 `json:"end_time_ms"` // Unix Epoch time in milliseconds
|
||||
Result string `json:"result"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type WorkspaceKind struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type DataVolumeModel struct {
|
||||
PvcName string `json:"pvc_name"`
|
||||
MountPath string `json:"mount_path"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
}
|
||||
|
|
@ -0,0 +1,328 @@
|
|||
/*
|
||||
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 workspaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
const (
|
||||
UnknownHomeMountPath = "__UNKNOWN_HOME_MOUNT_PATH__"
|
||||
UnknownImageConfig = "__UNKNOWN_IMAGE_CONFIG__"
|
||||
UnknownPodConfig = "__UNKNOWN_POD_CONFIG__"
|
||||
)
|
||||
|
||||
// NewWorkspaceModelFromWorkspace creates a Workspace model from a Workspace and WorkspaceKind object.
|
||||
// NOTE: the WorkspaceKind might not exist, so we handle the case where it is nil or has no UID.
|
||||
func NewWorkspaceModelFromWorkspace(ws *kubefloworgv1beta1.Workspace, wsk *kubefloworgv1beta1.WorkspaceKind) Workspace {
|
||||
// ensure the provided WorkspaceKind matches the Workspace
|
||||
if wskExists(wsk) && ws.Spec.Kind != wsk.Name {
|
||||
panic("provided WorkspaceKind does not match the Workspace")
|
||||
}
|
||||
|
||||
// TODO: icons can either be a remote URL or read from a ConfigMap.
|
||||
// in BOTH cases, we should cache and serve the image under a path on the backend API:
|
||||
// /api/v1/workspacekinds/{name}/assets/icon
|
||||
iconRef := ImageRef{
|
||||
URL: fmt.Sprintf("/workspaces/backend/api/v1/workspacekinds/%s/assets/icon", ws.Spec.Kind),
|
||||
}
|
||||
|
||||
// TODO: logos can either be a remote URL or read from a ConfigMap.
|
||||
// in BOTH cases, we should cache and serve the image under a path on the backend API:
|
||||
// /api/v1/workspacekinds/{name}/assets/logo
|
||||
logoRef := ImageRef{
|
||||
URL: fmt.Sprintf("/workspaces/backend/api/v1/workspacekinds/%s/assets/logo", ws.Spec.Kind),
|
||||
}
|
||||
|
||||
wsState := WorkspaceStateUnknown
|
||||
switch ws.Status.State {
|
||||
case kubefloworgv1beta1.WorkspaceStateRunning:
|
||||
wsState = WorkspaceStateRunning
|
||||
case kubefloworgv1beta1.WorkspaceStateTerminating:
|
||||
wsState = WorkspaceStateTerminating
|
||||
case kubefloworgv1beta1.WorkspaceStatePaused:
|
||||
wsState = WorkspaceStatePaused
|
||||
case kubefloworgv1beta1.WorkspaceStatePending:
|
||||
wsState = WorkspaceStatePending
|
||||
case kubefloworgv1beta1.WorkspaceStateError:
|
||||
wsState = WorkspaceStateError
|
||||
case kubefloworgv1beta1.WorkspaceStateUnknown:
|
||||
wsState = WorkspaceStateUnknown
|
||||
}
|
||||
|
||||
podLabels := make(map[string]string)
|
||||
podAnnotations := make(map[string]string)
|
||||
if ws.Spec.PodTemplate.PodMetadata != nil {
|
||||
// NOTE: we copy the maps to avoid creating a reference to the original maps.
|
||||
for k, v := range ws.Spec.PodTemplate.PodMetadata.Labels {
|
||||
podLabels[k] = v
|
||||
}
|
||||
for k, v := range ws.Spec.PodTemplate.PodMetadata.Annotations {
|
||||
podAnnotations[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
dataVolumes := make([]PodVolumeInfo, len(ws.Spec.PodTemplate.Volumes.Data))
|
||||
for i := range ws.Spec.PodTemplate.Volumes.Data {
|
||||
volume := ws.Spec.PodTemplate.Volumes.Data[i]
|
||||
readOnly := false
|
||||
if volume.ReadOnly != nil {
|
||||
readOnly = *volume.ReadOnly
|
||||
}
|
||||
dataVolumes[i] = PodVolumeInfo{
|
||||
PvcName: volume.PVCName,
|
||||
MountPath: volume.MountPath,
|
||||
ReadOnly: readOnly,
|
||||
}
|
||||
}
|
||||
|
||||
workspaceModel := Workspace{
|
||||
Name: ws.Name,
|
||||
Namespace: ws.Namespace,
|
||||
WorkspaceKind: WorkspaceKindInfo{
|
||||
Name: ws.Spec.Kind,
|
||||
Missing: !wskExists(wsk),
|
||||
Icon: iconRef,
|
||||
Logo: logoRef,
|
||||
},
|
||||
DeferUpdates: ptr.Deref(ws.Spec.DeferUpdates, false),
|
||||
Paused: ptr.Deref(ws.Spec.Paused, false),
|
||||
PausedTime: ws.Status.PauseTime,
|
||||
State: wsState,
|
||||
StateMessage: ws.Status.StateMessage,
|
||||
PodTemplate: PodTemplate{
|
||||
PodMetadata: PodMetadata{
|
||||
Labels: podLabels,
|
||||
Annotations: podAnnotations,
|
||||
},
|
||||
Volumes: PodVolumes{
|
||||
Home: buildHomeVolume(ws, wsk),
|
||||
Data: dataVolumes,
|
||||
},
|
||||
Options: PodTemplateOptions{
|
||||
ImageConfig: buildImageConfig(ws, wsk),
|
||||
PodConfig: buildPodConfig(ws, wsk),
|
||||
},
|
||||
},
|
||||
Activity: Activity{
|
||||
LastActivity: ws.Status.Activity.LastActivity,
|
||||
LastUpdate: ws.Status.Activity.LastUpdate,
|
||||
// TODO: populate LastProbe when culling is implemented:
|
||||
// https://github.com/kubeflow/notebooks/issues/38
|
||||
LastProbe: nil,
|
||||
},
|
||||
}
|
||||
return workspaceModel
|
||||
}
|
||||
|
||||
func wskExists(wsk *kubefloworgv1beta1.WorkspaceKind) bool {
|
||||
return wsk != nil && wsk.UID != ""
|
||||
}
|
||||
|
||||
func buildHomeVolume(ws *kubefloworgv1beta1.Workspace, wsk *kubefloworgv1beta1.WorkspaceKind) *PodVolumeInfo {
|
||||
if ws.Spec.PodTemplate.Volumes.Home == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// we only know the mount path if the WorkspaceKind exists
|
||||
homeMountPath := UnknownHomeMountPath
|
||||
if wskExists(wsk) {
|
||||
homeMountPath = wsk.Spec.PodTemplate.VolumeMounts.Home
|
||||
}
|
||||
|
||||
return &PodVolumeInfo{
|
||||
PvcName: *ws.Spec.PodTemplate.Volumes.Home,
|
||||
MountPath: homeMountPath,
|
||||
// the home volume is ~always~ read-write
|
||||
ReadOnly: false,
|
||||
}
|
||||
}
|
||||
|
||||
func buildImageConfig(ws *kubefloworgv1beta1.Workspace, wsk *kubefloworgv1beta1.WorkspaceKind) ImageConfig {
|
||||
// create a map of image configs from the WorkspaceKind for easy lookup by ID
|
||||
// NOTE: we can only build this map if the WorkspaceKind exists, otherwise it will be empty
|
||||
imageConfigMap := make(map[string]kubefloworgv1beta1.ImageConfigValue)
|
||||
if wskExists(wsk) {
|
||||
imageConfigMap = make(map[string]kubefloworgv1beta1.ImageConfigValue, len(wsk.Spec.PodTemplate.Options.ImageConfig.Values))
|
||||
for _, value := range wsk.Spec.PodTemplate.Options.ImageConfig.Values {
|
||||
imageConfigMap[value.Id] = value
|
||||
}
|
||||
}
|
||||
|
||||
// get the current image config
|
||||
currentImageConfig := OptionInfo{
|
||||
Id: ws.Spec.PodTemplate.Options.ImageConfig,
|
||||
DisplayName: UnknownImageConfig,
|
||||
Description: UnknownImageConfig,
|
||||
Labels: nil,
|
||||
}
|
||||
if cfg, ok := imageConfigMap[currentImageConfig.Id]; ok {
|
||||
currentImageConfig.DisplayName = cfg.Spawner.DisplayName
|
||||
currentImageConfig.Description = ptr.Deref(cfg.Spawner.Description, "")
|
||||
currentImageConfig.Labels = buildOptionLabels(cfg.Spawner.Labels)
|
||||
}
|
||||
|
||||
// get the desired image config
|
||||
// NOTE: the desired image config will be nil if it is the same as the current image config
|
||||
var desiredImageConfig *OptionInfo
|
||||
desiredImageConfigId := ws.Status.PodTemplateOptions.ImageConfig.Desired
|
||||
if desiredImageConfigId != "" && desiredImageConfigId != currentImageConfig.Id {
|
||||
desiredImageConfig = &OptionInfo{
|
||||
Id: desiredImageConfigId,
|
||||
DisplayName: UnknownImageConfig,
|
||||
Description: UnknownImageConfig,
|
||||
Labels: nil,
|
||||
}
|
||||
if cfg, ok := imageConfigMap[desiredImageConfig.Id]; ok {
|
||||
desiredImageConfig.DisplayName = cfg.Spawner.DisplayName
|
||||
desiredImageConfig.Description = ptr.Deref(cfg.Spawner.Description, "")
|
||||
desiredImageConfig.Labels = buildOptionLabels(cfg.Spawner.Labels)
|
||||
}
|
||||
}
|
||||
|
||||
// build the redirect chain
|
||||
// NOTE: the redirect chain will be nil (not an empty slice) if there are no redirects
|
||||
var redirectChain []RedirectStep
|
||||
numRedirects := len(ws.Status.PodTemplateOptions.ImageConfig.RedirectChain)
|
||||
if numRedirects > 0 {
|
||||
redirectChain = make([]RedirectStep, numRedirects)
|
||||
}
|
||||
for i := range ws.Status.PodTemplateOptions.ImageConfig.RedirectChain {
|
||||
step := ws.Status.PodTemplateOptions.ImageConfig.RedirectChain[i]
|
||||
redirectChain[i] = RedirectStep{
|
||||
SourceId: step.Source,
|
||||
TargetId: step.Target,
|
||||
}
|
||||
if cfg, ok := imageConfigMap[step.Source]; ok {
|
||||
// skip the redirect if it's not the target we expect
|
||||
if cfg.Redirect != nil && cfg.Redirect.To == step.Target {
|
||||
redirectChain[i].Message = buildRedirectMessage(cfg.Redirect.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ImageConfig{
|
||||
Current: currentImageConfig,
|
||||
Desired: desiredImageConfig,
|
||||
RedirectChain: redirectChain,
|
||||
}
|
||||
}
|
||||
|
||||
func buildPodConfig(ws *kubefloworgv1beta1.Workspace, wsk *kubefloworgv1beta1.WorkspaceKind) PodConfig {
|
||||
// create a map of pod configs from the WorkspaceKind for easy lookup by ID
|
||||
// NOTE: we can only build this map if the WorkspaceKind exists, otherwise it will be empty
|
||||
podConfigMap := make(map[string]kubefloworgv1beta1.PodConfigValue)
|
||||
if wskExists(wsk) {
|
||||
podConfigMap = make(map[string]kubefloworgv1beta1.PodConfigValue, len(wsk.Spec.PodTemplate.Options.PodConfig.Values))
|
||||
for _, value := range wsk.Spec.PodTemplate.Options.PodConfig.Values {
|
||||
podConfigMap[value.Id] = value
|
||||
}
|
||||
}
|
||||
|
||||
// get the current pod config
|
||||
currentPodConfig := OptionInfo{
|
||||
Id: ws.Spec.PodTemplate.Options.PodConfig,
|
||||
DisplayName: UnknownPodConfig,
|
||||
Description: UnknownPodConfig,
|
||||
Labels: nil,
|
||||
}
|
||||
if cfg, ok := podConfigMap[currentPodConfig.Id]; ok {
|
||||
currentPodConfig.DisplayName = cfg.Spawner.DisplayName
|
||||
currentPodConfig.Description = ptr.Deref(cfg.Spawner.Description, "")
|
||||
currentPodConfig.Labels = buildOptionLabels(cfg.Spawner.Labels)
|
||||
}
|
||||
|
||||
// get the desired pod config
|
||||
// NOTE: the desired pod config will be nil if it is the same as the current pod config
|
||||
var desiredPodConfig *OptionInfo
|
||||
desiredPodConfigId := ws.Status.PodTemplateOptions.PodConfig.Desired
|
||||
if desiredPodConfigId != "" && desiredPodConfigId != currentPodConfig.Id {
|
||||
desiredPodConfig = &OptionInfo{
|
||||
Id: desiredPodConfigId,
|
||||
DisplayName: UnknownPodConfig,
|
||||
Description: UnknownPodConfig,
|
||||
Labels: nil,
|
||||
}
|
||||
if cfg, ok := podConfigMap[desiredPodConfig.Id]; ok {
|
||||
desiredPodConfig.DisplayName = cfg.Spawner.DisplayName
|
||||
desiredPodConfig.Description = ptr.Deref(cfg.Spawner.Description, "")
|
||||
desiredPodConfig.Labels = buildOptionLabels(cfg.Spawner.Labels)
|
||||
}
|
||||
}
|
||||
|
||||
// build the redirect chain
|
||||
// NOTE: the redirect chain will be nil (not an empty slice) if there are no redirects
|
||||
var redirectChain []RedirectStep
|
||||
numRedirects := len(ws.Status.PodTemplateOptions.PodConfig.RedirectChain)
|
||||
if numRedirects > 0 {
|
||||
redirectChain = make([]RedirectStep, numRedirects)
|
||||
}
|
||||
for i := range ws.Status.PodTemplateOptions.PodConfig.RedirectChain {
|
||||
step := ws.Status.PodTemplateOptions.PodConfig.RedirectChain[i]
|
||||
redirectChain[i] = RedirectStep{
|
||||
SourceId: step.Source,
|
||||
TargetId: step.Target,
|
||||
}
|
||||
if cfg, ok := podConfigMap[step.Source]; ok {
|
||||
// skip the redirect if it's not the target we expect
|
||||
if cfg.Redirect != nil && cfg.Redirect.To == step.Target {
|
||||
redirectChain[i].Message = buildRedirectMessage(cfg.Redirect.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PodConfig{
|
||||
Current: currentPodConfig,
|
||||
Desired: desiredPodConfig,
|
||||
RedirectChain: redirectChain,
|
||||
}
|
||||
}
|
||||
|
||||
func buildOptionLabels(labels []kubefloworgv1beta1.OptionSpawnerLabel) []OptionLabel {
|
||||
optionLabels := make([]OptionLabel, len(labels))
|
||||
for i := range labels {
|
||||
optionLabels[i] = OptionLabel{
|
||||
Key: labels[i].Key,
|
||||
Value: labels[i].Value,
|
||||
}
|
||||
}
|
||||
return optionLabels
|
||||
}
|
||||
|
||||
func buildRedirectMessage(msg *kubefloworgv1beta1.RedirectMessage) *RedirectMessage {
|
||||
if msg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
messageLevel := RedirectMessageLevelInfo
|
||||
switch msg.Level {
|
||||
case kubefloworgv1beta1.RedirectMessageLevelInfo:
|
||||
messageLevel = RedirectMessageLevelInfo
|
||||
case kubefloworgv1beta1.RedirectMessageLevelWarning:
|
||||
messageLevel = RedirectMessageLevelWarning
|
||||
case kubefloworgv1beta1.RedirectMessageLevelDanger:
|
||||
messageLevel = RedirectMessageLevelDanger
|
||||
}
|
||||
|
||||
return &RedirectMessage{
|
||||
Text: msg.Text,
|
||||
Level: messageLevel,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
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 workspaces
|
||||
|
||||
type Workspace struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
WorkspaceKind WorkspaceKindInfo `json:"workspace_kind"`
|
||||
DeferUpdates bool `json:"defer_updates"`
|
||||
Paused bool `json:"paused"`
|
||||
PausedTime int64 `json:"paused_time"`
|
||||
State WorkspaceState `json:"state"`
|
||||
StateMessage string `json:"state_message"`
|
||||
PodTemplate PodTemplate `json:"pod_template"`
|
||||
Activity Activity `json:"activity"`
|
||||
}
|
||||
|
||||
type WorkspaceState string
|
||||
|
||||
const (
|
||||
WorkspaceStateRunning WorkspaceState = "Running"
|
||||
WorkspaceStateTerminating WorkspaceState = "Terminating"
|
||||
WorkspaceStatePaused WorkspaceState = "Paused"
|
||||
WorkspaceStatePending WorkspaceState = "Pending"
|
||||
WorkspaceStateError WorkspaceState = "Error"
|
||||
WorkspaceStateUnknown WorkspaceState = "Unknown"
|
||||
)
|
||||
|
||||
type WorkspaceKindInfo struct {
|
||||
Name string `json:"name"`
|
||||
Missing bool `json:"missing"`
|
||||
Icon ImageRef `json:"icon"`
|
||||
Logo ImageRef `json:"logo"`
|
||||
}
|
||||
|
||||
type ImageRef struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type PodTemplate struct {
|
||||
PodMetadata PodMetadata `json:"pod_metadata"`
|
||||
Volumes PodVolumes `json:"volumes"`
|
||||
Options PodTemplateOptions `json:"options"`
|
||||
}
|
||||
|
||||
type PodMetadata struct {
|
||||
Labels map[string]string `json:"labels"`
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
}
|
||||
|
||||
type PodVolumes struct {
|
||||
Home *PodVolumeInfo `json:"home,omitempty"`
|
||||
Data []PodVolumeInfo `json:"data"`
|
||||
}
|
||||
|
||||
type PodVolumeInfo struct {
|
||||
PvcName string `json:"pvc_name"`
|
||||
MountPath string `json:"mount_path"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
}
|
||||
|
||||
type PodTemplateOptions struct {
|
||||
ImageConfig ImageConfig `json:"image_config"`
|
||||
PodConfig PodConfig `json:"pod_config"`
|
||||
}
|
||||
|
||||
type ImageConfig struct {
|
||||
Current OptionInfo `json:"current"`
|
||||
Desired *OptionInfo `json:"desired,omitempty"`
|
||||
RedirectChain []RedirectStep `json:"redirect_chain,omitempty"`
|
||||
}
|
||||
|
||||
type PodConfig struct {
|
||||
Current OptionInfo `json:"current"`
|
||||
Desired *OptionInfo `json:"desired,omitempty"`
|
||||
RedirectChain []RedirectStep `json:"redirect_chain,omitempty"`
|
||||
}
|
||||
|
||||
type OptionInfo struct {
|
||||
Id string `json:"id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Description string `json:"description"`
|
||||
Labels []OptionLabel `json:"labels"`
|
||||
}
|
||||
|
||||
type OptionLabel struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type RedirectStep struct {
|
||||
SourceId string `json:"source_id"`
|
||||
TargetId string `json:"target_id"`
|
||||
Message *RedirectMessage `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type RedirectMessage struct {
|
||||
Text string `json:"text"`
|
||||
Level RedirectMessageLevel `json:"level"`
|
||||
}
|
||||
|
||||
type RedirectMessageLevel string
|
||||
|
||||
const (
|
||||
RedirectMessageLevelInfo RedirectMessageLevel = "Info"
|
||||
RedirectMessageLevelWarning RedirectMessageLevel = "Warning"
|
||||
RedirectMessageLevelDanger RedirectMessageLevel = "Danger"
|
||||
)
|
||||
|
||||
type Activity struct {
|
||||
LastActivity int64 `json:"last_activity"` // Unix Epoch time
|
||||
LastUpdate int64 `json:"last_update"` // Unix Epoch time
|
||||
LastProbe *LastProbeInfo `json:"last_probe,omitempty"`
|
||||
}
|
||||
|
||||
type LastProbeInfo struct {
|
||||
StartTimeMs int64 `json:"start_time_ms"` // Unix Epoch time in milliseconds
|
||||
EndTimeMs int64 `json:"end_time_ms"` // Unix Epoch time in milliseconds
|
||||
Result ProbeResult `json:"result"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type ProbeResult string
|
||||
|
||||
const (
|
||||
ProbeResultSuccess ProbeResult = "Success"
|
||||
ProbeResultFailure ProbeResult = "Failure"
|
||||
ProbeResultTimeout ProbeResult = "Timeout"
|
||||
)
|
||||
|
|
@ -14,9 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package repositories
|
||||
package health_check
|
||||
|
||||
import "github.com/kubeflow/notebooks/workspaces/backend/internal/models"
|
||||
import (
|
||||
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/health_check"
|
||||
)
|
||||
|
||||
type HealthCheckRepository struct{}
|
||||
|
||||
|
|
@ -24,10 +26,11 @@ func NewHealthCheckRepository() *HealthCheckRepository {
|
|||
return &HealthCheckRepository{}
|
||||
}
|
||||
|
||||
func (r *HealthCheckRepository) HealthCheck(version string) (models.HealthCheckModel, error) {
|
||||
func (r *HealthCheckRepository) HealthCheck(version string) (models.HealthCheck, error) {
|
||||
|
||||
var res = models.HealthCheckModel{
|
||||
Status: "available",
|
||||
var res = models.HealthCheck{
|
||||
// TODO: implement actual health check logic
|
||||
Status: models.ServiceStatusHealthy,
|
||||
SystemInfo: models.SystemInfo{
|
||||
Version: version,
|
||||
},
|
||||
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package repositories
|
||||
package namespaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -22,7 +22,7 @@ import (
|
|||
v1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/models"
|
||||
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/namespaces"
|
||||
)
|
||||
|
||||
type NamespaceRepository struct {
|
||||
|
|
@ -35,7 +35,7 @@ func NewNamespaceRepository(cl client.Client) *NamespaceRepository {
|
|||
}
|
||||
}
|
||||
|
||||
func (r *NamespaceRepository) GetNamespaces(ctx context.Context) ([]models.NamespaceModel, error) {
|
||||
func (r *NamespaceRepository) GetNamespaces(ctx context.Context) ([]models.Namespace, error) {
|
||||
|
||||
// TODO(ederign): Implement subject access review here to fetch only
|
||||
// namespaces that "kubeflow-userid" has access to
|
||||
|
|
@ -45,9 +45,10 @@ func (r *NamespaceRepository) GetNamespaces(ctx context.Context) ([]models.Names
|
|||
return nil, err
|
||||
}
|
||||
|
||||
namespaces := make([]models.NamespaceModel, len(namespaceList.Items))
|
||||
for i, ns := range namespaceList.Items {
|
||||
namespaces[i] = models.NewNamespaceModelFromNamespace(ns.Name)
|
||||
namespaces := make([]models.Namespace, len(namespaceList.Items))
|
||||
for i := range namespaceList.Items {
|
||||
namespace := &namespaceList.Items[i]
|
||||
namespaces[i] = models.NewNamespaceModelFromNamespace(namespace)
|
||||
}
|
||||
return namespaces, nil
|
||||
}
|
||||
|
|
@ -18,21 +18,27 @@ package repositories
|
|||
|
||||
import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/health_check"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/namespaces"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/workspacekinds"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/workspaces"
|
||||
)
|
||||
|
||||
// Models struct is a single convenient container to hold and represent all our repositories.
|
||||
// Repositories is a single convenient container to hold and represent all our repositories.
|
||||
type Repositories struct {
|
||||
HealthCheck *HealthCheckRepository
|
||||
Workspace *WorkspaceRepository
|
||||
WorkspaceKind *WorkspaceKindRepository
|
||||
Namespace *NamespaceRepository
|
||||
HealthCheck *health_check.HealthCheckRepository
|
||||
Namespace *namespaces.NamespaceRepository
|
||||
Workspace *workspaces.WorkspaceRepository
|
||||
WorkspaceKind *workspacekinds.WorkspaceKindRepository
|
||||
}
|
||||
|
||||
// NewRepositories creates a new Repositories instance from a controller-runtime client.
|
||||
func NewRepositories(cl client.Client) *Repositories {
|
||||
return &Repositories{
|
||||
HealthCheck: NewHealthCheckRepository(),
|
||||
Workspace: NewWorkspaceRepository(cl),
|
||||
WorkspaceKind: NewWorkspaceKindRepository(cl),
|
||||
Namespace: NewNamespaceRepository(cl),
|
||||
HealthCheck: health_check.NewHealthCheckRepository(),
|
||||
Namespace: namespaces.NewNamespaceRepository(cl),
|
||||
Workspace: workspaces.NewWorkspaceRepository(cl),
|
||||
WorkspaceKind: workspacekinds.NewWorkspaceKindRepository(cl),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package repositories
|
||||
package workspacekinds
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -24,7 +24,7 @@ import (
|
|||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/models"
|
||||
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspacekinds"
|
||||
)
|
||||
|
||||
var ErrWorkspaceKindNotFound = errors.New("workspace kind not found")
|
||||
|
|
@ -39,21 +39,21 @@ func NewWorkspaceKindRepository(cl client.Client) *WorkspaceKindRepository {
|
|||
}
|
||||
}
|
||||
|
||||
func (r *WorkspaceKindRepository) GetWorkspaceKind(ctx context.Context, name string) (models.WorkspaceKindModel, error) {
|
||||
func (r *WorkspaceKindRepository) GetWorkspaceKind(ctx context.Context, name string) (models.WorkspaceKind, error) {
|
||||
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
err := r.client.Get(ctx, client.ObjectKey{Name: name}, workspaceKind)
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return models.WorkspaceKindModel{}, ErrWorkspaceKindNotFound
|
||||
return models.WorkspaceKind{}, ErrWorkspaceKindNotFound
|
||||
}
|
||||
return models.WorkspaceKindModel{}, err
|
||||
return models.WorkspaceKind{}, err
|
||||
}
|
||||
|
||||
workspaceKindModel := models.NewWorkspaceKindModelFromWorkspaceKind(workspaceKind)
|
||||
return workspaceKindModel, nil
|
||||
}
|
||||
|
||||
func (r *WorkspaceKindRepository) GetWorkspaceKinds(ctx context.Context) ([]models.WorkspaceKindModel, error) {
|
||||
func (r *WorkspaceKindRepository) GetWorkspaceKinds(ctx context.Context) ([]models.WorkspaceKind, error) {
|
||||
workspaceKindList := &kubefloworgv1beta1.WorkspaceKindList{}
|
||||
|
||||
err := r.client.List(ctx, workspaceKindList)
|
||||
|
|
@ -61,10 +61,10 @@ func (r *WorkspaceKindRepository) GetWorkspaceKinds(ctx context.Context) ([]mode
|
|||
return nil, err
|
||||
}
|
||||
|
||||
workspaceKindsModels := make([]models.WorkspaceKindModel, len(workspaceKindList.Items))
|
||||
for i, item := range workspaceKindList.Items {
|
||||
workspaceKindModel := models.NewWorkspaceKindModelFromWorkspaceKind(&item)
|
||||
workspaceKindsModels[i] = workspaceKindModel
|
||||
workspaceKindsModels := make([]models.WorkspaceKind, len(workspaceKindList.Items))
|
||||
for i := range workspaceKindList.Items {
|
||||
workspaceKind := &workspaceKindList.Items[i]
|
||||
workspaceKindsModels[i] = models.NewWorkspaceKindModelFromWorkspaceKind(workspaceKind)
|
||||
}
|
||||
|
||||
return workspaceKindsModels, nil
|
||||
|
|
@ -1,259 +0,0 @@
|
|||
/*
|
||||
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 repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/models"
|
||||
)
|
||||
|
||||
var ErrWorkspaceNotFound = errors.New("workspace not found")
|
||||
|
||||
type WorkspaceRepository struct {
|
||||
client client.Client
|
||||
}
|
||||
|
||||
func NewWorkspaceRepository(cl client.Client) *WorkspaceRepository {
|
||||
return &WorkspaceRepository{
|
||||
client: cl,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *WorkspaceRepository) GetWorkspace(ctx context.Context, namespace string, workspaceName string) (models.WorkspaceModel, error) {
|
||||
workspace := &kubefloworgv1beta1.Workspace{}
|
||||
if err := r.client.Get(ctx, client.ObjectKey{Namespace: namespace, Name: workspaceName}, workspace); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return models.WorkspaceModel{}, ErrWorkspaceNotFound
|
||||
}
|
||||
return models.WorkspaceModel{}, err
|
||||
}
|
||||
|
||||
kind := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
if err := r.client.Get(ctx, client.ObjectKey{Name: workspace.Spec.Kind}, kind); err != nil {
|
||||
return models.WorkspaceModel{}, err
|
||||
}
|
||||
|
||||
return NewWorkspaceModelFromWorkspace(workspace, kind), nil
|
||||
}
|
||||
|
||||
func (r *WorkspaceRepository) GetWorkspaces(ctx context.Context, namespace string) ([]models.WorkspaceModel, error) {
|
||||
workspaceList := &kubefloworgv1beta1.WorkspaceList{}
|
||||
listOptions := []client.ListOption{
|
||||
client.InNamespace(namespace),
|
||||
}
|
||||
err := r.client.List(ctx, workspaceList, listOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workspacesModels := make([]models.WorkspaceModel, len(workspaceList.Items))
|
||||
for i, item := range workspaceList.Items {
|
||||
kind := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
if err := r.client.Get(ctx, client.ObjectKey{Name: item.Spec.Kind}, kind); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
workspacesModels[i] = NewWorkspaceModelFromWorkspace(&item, kind)
|
||||
}
|
||||
|
||||
return workspacesModels, nil
|
||||
}
|
||||
|
||||
func (r *WorkspaceRepository) GetAllWorkspaces(ctx context.Context) ([]models.WorkspaceModel, error) {
|
||||
workspaceList := &kubefloworgv1beta1.WorkspaceList{}
|
||||
if err := r.client.List(ctx, workspaceList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workspacesModels := make([]models.WorkspaceModel, len(workspaceList.Items))
|
||||
for i, item := range workspaceList.Items {
|
||||
kind := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
if err := r.client.Get(ctx, client.ObjectKey{Name: item.Spec.Kind}, kind); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
workspacesModels[i] = NewWorkspaceModelFromWorkspace(&item, kind)
|
||||
}
|
||||
|
||||
return workspacesModels, nil
|
||||
}
|
||||
|
||||
func (r *WorkspaceRepository) CreateWorkspace(ctx context.Context, workspaceModel *models.WorkspaceModel) (models.WorkspaceModel, error) {
|
||||
// TODO: review all fields
|
||||
workspace := &kubefloworgv1beta1.Workspace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: workspaceModel.Name,
|
||||
Namespace: workspaceModel.Namespace,
|
||||
// TODO: the pod and workspace labels should be separated
|
||||
Labels: workspaceModel.PodTemplate.PodMetadata.Labels,
|
||||
Annotations: workspaceModel.PodTemplate.PodMetadata.Annotations,
|
||||
},
|
||||
Spec: kubefloworgv1beta1.WorkspaceSpec{
|
||||
Paused: &workspaceModel.Paused,
|
||||
DeferUpdates: &workspaceModel.DeferUpdates,
|
||||
// TODO: verify if workspace kind exists on validation
|
||||
Kind: workspaceModel.WorkspaceKind.Name,
|
||||
PodTemplate: kubefloworgv1beta1.WorkspacePodTemplate{
|
||||
PodMetadata: &kubefloworgv1beta1.WorkspacePodMetadata{
|
||||
Labels: workspaceModel.PodTemplate.PodMetadata.Labels,
|
||||
Annotations: workspaceModel.PodTemplate.PodMetadata.Annotations,
|
||||
},
|
||||
Volumes: kubefloworgv1beta1.WorkspacePodVolumes{
|
||||
Home: &workspaceModel.PodTemplate.Volumes.Home.PvcName,
|
||||
Data: []kubefloworgv1beta1.PodVolumeMount{},
|
||||
},
|
||||
Options: kubefloworgv1beta1.WorkspacePodOptions{
|
||||
ImageConfig: workspaceModel.PodTemplate.ImageConfig.Current,
|
||||
PodConfig: workspaceModel.PodTemplate.PodConfig.Current,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: create data volumes if necessary
|
||||
workspace.Spec.PodTemplate.Volumes.Data = make([]kubefloworgv1beta1.PodVolumeMount, len(workspaceModel.PodTemplate.Volumes.Data))
|
||||
for i, dataVolume := range workspaceModel.PodTemplate.Volumes.Data {
|
||||
// make a copy of readOnly because dataVolume is reassigned each loop
|
||||
readOnly := dataVolume.ReadOnly
|
||||
workspace.Spec.PodTemplate.Volumes.Data[i] = kubefloworgv1beta1.PodVolumeMount{
|
||||
PVCName: dataVolume.PvcName,
|
||||
MountPath: dataVolume.MountPath,
|
||||
ReadOnly: &readOnly,
|
||||
}
|
||||
}
|
||||
if err := r.client.Create(ctx, workspace); err != nil {
|
||||
return models.WorkspaceModel{}, err
|
||||
}
|
||||
|
||||
kind := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
if err := r.client.Get(ctx, client.ObjectKey{Name: workspace.Spec.Kind}, kind); err != nil {
|
||||
return models.WorkspaceModel{}, err
|
||||
}
|
||||
|
||||
return NewWorkspaceModelFromWorkspace(workspace, kind), nil
|
||||
}
|
||||
|
||||
func (r *WorkspaceRepository) DeleteWorkspace(ctx context.Context, namespace, workspaceName string) error {
|
||||
workspace := &kubefloworgv1beta1.Workspace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
Name: workspaceName,
|
||||
},
|
||||
}
|
||||
|
||||
if err := r.client.Delete(ctx, workspace); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return ErrWorkspaceNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewWorkspaceModelFromWorkspace(item *kubefloworgv1beta1.Workspace, wsk *kubefloworgv1beta1.WorkspaceKind) models.WorkspaceModel {
|
||||
dataVolumes := make([]models.DataVolumeModel, len(item.Spec.PodTemplate.Volumes.Data))
|
||||
for i, volume := range item.Spec.PodTemplate.Volumes.Data {
|
||||
dataVolumes[i] = models.DataVolumeModel{
|
||||
PvcName: volume.PVCName,
|
||||
MountPath: volume.MountPath,
|
||||
ReadOnly: *volume.ReadOnly,
|
||||
}
|
||||
}
|
||||
|
||||
imageConfigRedirectChain := make([]*models.RedirectChain, len(item.Status.PodTemplateOptions.ImageConfig.RedirectChain))
|
||||
for i, chain := range item.Status.PodTemplateOptions.ImageConfig.RedirectChain {
|
||||
imageConfigRedirectChain[i] = &models.RedirectChain{
|
||||
Source: chain.Source,
|
||||
Target: chain.Target,
|
||||
}
|
||||
}
|
||||
|
||||
podConfigRedirectChain := make([]*models.RedirectChain, len(item.Status.PodTemplateOptions.PodConfig.RedirectChain))
|
||||
|
||||
for i, chain := range item.Status.PodTemplateOptions.PodConfig.RedirectChain {
|
||||
podConfigRedirectChain[i] = &models.RedirectChain{
|
||||
Source: chain.Source,
|
||||
Target: chain.Target,
|
||||
}
|
||||
}
|
||||
|
||||
podMetadataLabels := item.Spec.PodTemplate.PodMetadata.Labels
|
||||
if podMetadataLabels == nil {
|
||||
podMetadataLabels = map[string]string{}
|
||||
}
|
||||
|
||||
podMetadataAnnotations := item.Spec.PodTemplate.PodMetadata.Annotations
|
||||
if podMetadataAnnotations == nil {
|
||||
podMetadataAnnotations = map[string]string{}
|
||||
}
|
||||
|
||||
workspaceModel := models.WorkspaceModel{
|
||||
Name: item.ObjectMeta.Name,
|
||||
Namespace: item.Namespace,
|
||||
WorkspaceKind: models.WorkspaceKind{
|
||||
Name: item.Spec.Kind,
|
||||
Type: "POD_TEMPLATE",
|
||||
},
|
||||
DeferUpdates: *item.Spec.DeferUpdates,
|
||||
Paused: *item.Spec.Paused,
|
||||
PausedTime: item.Status.PauseTime,
|
||||
State: string(item.Status.State),
|
||||
StateMessage: item.Status.StateMessage,
|
||||
PodTemplate: models.PodTemplate{
|
||||
PodMetadata: &models.PodMetadata{
|
||||
Labels: podMetadataLabels,
|
||||
Annotations: podMetadataAnnotations,
|
||||
},
|
||||
Volumes: &models.Volumes{
|
||||
Home: &models.DataVolumeModel{
|
||||
PvcName: *item.Spec.PodTemplate.Volumes.Home,
|
||||
MountPath: wsk.Spec.PodTemplate.VolumeMounts.Home,
|
||||
ReadOnly: false, // From where to get this value?
|
||||
},
|
||||
Data: dataVolumes,
|
||||
},
|
||||
ImageConfig: &models.ImageConfig{
|
||||
Current: item.Spec.PodTemplate.Options.ImageConfig,
|
||||
Desired: item.Status.PodTemplateOptions.ImageConfig.Desired,
|
||||
RedirectChain: imageConfigRedirectChain,
|
||||
},
|
||||
PodConfig: &models.PodConfig{
|
||||
Current: item.Spec.PodTemplate.Options.PodConfig,
|
||||
Desired: item.Spec.PodTemplate.Options.PodConfig,
|
||||
RedirectChain: podConfigRedirectChain,
|
||||
},
|
||||
},
|
||||
Activity: models.Activity{
|
||||
LastActivity: item.Status.Activity.LastActivity,
|
||||
LastUpdate: item.Status.Activity.LastUpdate,
|
||||
// TODO: update these fields when the last probe is implemented
|
||||
LastProbe: &models.Probe{
|
||||
StartTimeMs: 0,
|
||||
EndTimeMs: 0,
|
||||
Result: "default_result",
|
||||
Message: "default_message",
|
||||
},
|
||||
},
|
||||
}
|
||||
return workspaceModel
|
||||
}
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
/*
|
||||
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 workspaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/utils/ptr"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspaces"
|
||||
)
|
||||
|
||||
var ErrWorkspaceNotFound = fmt.Errorf("workspace not found")
|
||||
var ErrRefWorkspaceKindNotExists = fmt.Errorf("referenced WorkspaceKind does not exist")
|
||||
|
||||
type WorkspaceRepository struct {
|
||||
client client.Client
|
||||
}
|
||||
|
||||
func NewWorkspaceRepository(cl client.Client) *WorkspaceRepository {
|
||||
return &WorkspaceRepository{
|
||||
client: cl,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *WorkspaceRepository) GetWorkspace(ctx context.Context, namespace string, workspaceName string) (models.Workspace, error) {
|
||||
// get workspace
|
||||
workspace := &kubefloworgv1beta1.Workspace{}
|
||||
if err := r.client.Get(ctx, client.ObjectKey{Namespace: namespace, Name: workspaceName}, workspace); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return models.Workspace{}, ErrWorkspaceNotFound
|
||||
}
|
||||
return models.Workspace{}, err
|
||||
}
|
||||
|
||||
// get workspace kind, if it exists
|
||||
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
workspaceKindName := workspace.Spec.Kind
|
||||
if err := r.client.Get(ctx, client.ObjectKey{Name: workspaceKindName}, workspaceKind); err != nil {
|
||||
// ignore error if workspace kind does not exist, as we can still create a model without it
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return models.Workspace{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// convert workspace to model
|
||||
workspaceModel := models.NewWorkspaceModelFromWorkspace(workspace, workspaceKind)
|
||||
|
||||
return workspaceModel, nil
|
||||
}
|
||||
|
||||
func (r *WorkspaceRepository) GetWorkspaces(ctx context.Context, namespace string) ([]models.Workspace, error) {
|
||||
// get all workspaces in the namespace
|
||||
workspaceList := &kubefloworgv1beta1.WorkspaceList{}
|
||||
listOptions := []client.ListOption{
|
||||
client.InNamespace(namespace),
|
||||
}
|
||||
err := r.client.List(ctx, workspaceList, listOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// convert workspaces to models
|
||||
workspacesModels := make([]models.Workspace, len(workspaceList.Items))
|
||||
for i, workspace := range workspaceList.Items {
|
||||
|
||||
// get workspace kind, if it exists
|
||||
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
workspaceKindName := workspace.Spec.Kind
|
||||
if err := r.client.Get(ctx, client.ObjectKey{Name: workspaceKindName}, workspaceKind); err != nil {
|
||||
// ignore error if workspace kind does not exist, as we can still create a model without it
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
workspacesModels[i] = models.NewWorkspaceModelFromWorkspace(&workspace, workspaceKind)
|
||||
}
|
||||
|
||||
return workspacesModels, nil
|
||||
}
|
||||
|
||||
func (r *WorkspaceRepository) GetAllWorkspaces(ctx context.Context) ([]models.Workspace, error) {
|
||||
// get all workspaces in the cluster
|
||||
workspaceList := &kubefloworgv1beta1.WorkspaceList{}
|
||||
if err := r.client.List(ctx, workspaceList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// convert workspaces to models
|
||||
workspacesModels := make([]models.Workspace, len(workspaceList.Items))
|
||||
for i, workspace := range workspaceList.Items {
|
||||
|
||||
// get workspace kind, if it exists
|
||||
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
workspaceKindName := workspace.Spec.Kind
|
||||
if err := r.client.Get(ctx, client.ObjectKey{Name: workspaceKindName}, workspaceKind); err != nil {
|
||||
// ignore error if workspace kind does not exist, as we can still create a model without it
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
workspacesModels[i] = models.NewWorkspaceModelFromWorkspace(&workspace, workspaceKind)
|
||||
}
|
||||
|
||||
return workspacesModels, nil
|
||||
}
|
||||
|
||||
func (r *WorkspaceRepository) CreateWorkspace(ctx context.Context, workspaceModel *models.Workspace) (models.Workspace, error) {
|
||||
// get workspace kind
|
||||
// NOTE: if the referenced workspace kind does not exist,
|
||||
// we throw an error because the api would reject the workspace creation
|
||||
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{}
|
||||
workspaceKindName := workspaceModel.WorkspaceKind.Name
|
||||
if err := r.client.Get(ctx, client.ObjectKey{Name: workspaceKindName}, workspaceKind); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return models.Workspace{}, fmt.Errorf("%w: %s", ErrRefWorkspaceKindNotExists, workspaceKindName)
|
||||
}
|
||||
return models.Workspace{}, err
|
||||
}
|
||||
|
||||
// get data volumes from workspace model
|
||||
dataVolumeMounts := make([]kubefloworgv1beta1.PodVolumeMount, len(workspaceModel.PodTemplate.Volumes.Data))
|
||||
for i, dataVolume := range workspaceModel.PodTemplate.Volumes.Data {
|
||||
dataVolumeMounts[i] = kubefloworgv1beta1.PodVolumeMount{
|
||||
PVCName: dataVolume.PvcName,
|
||||
MountPath: dataVolume.MountPath,
|
||||
ReadOnly: ptr.To(dataVolume.ReadOnly),
|
||||
}
|
||||
}
|
||||
|
||||
// define workspace object from model
|
||||
workspace := &kubefloworgv1beta1.Workspace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: workspaceModel.Name,
|
||||
Namespace: workspaceModel.Namespace,
|
||||
},
|
||||
Spec: kubefloworgv1beta1.WorkspaceSpec{
|
||||
Paused: &workspaceModel.Paused,
|
||||
DeferUpdates: &workspaceModel.DeferUpdates,
|
||||
Kind: workspaceKindName,
|
||||
PodTemplate: kubefloworgv1beta1.WorkspacePodTemplate{
|
||||
PodMetadata: &kubefloworgv1beta1.WorkspacePodMetadata{
|
||||
Labels: workspaceModel.PodTemplate.PodMetadata.Labels,
|
||||
Annotations: workspaceModel.PodTemplate.PodMetadata.Annotations,
|
||||
},
|
||||
Volumes: kubefloworgv1beta1.WorkspacePodVolumes{
|
||||
Home: &workspaceModel.PodTemplate.Volumes.Home.PvcName,
|
||||
Data: dataVolumeMounts,
|
||||
},
|
||||
Options: kubefloworgv1beta1.WorkspacePodOptions{
|
||||
ImageConfig: workspaceModel.PodTemplate.Options.ImageConfig.Current.Id,
|
||||
PodConfig: workspaceModel.PodTemplate.Options.PodConfig.Current.Id,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// create workspace
|
||||
if err := r.client.Create(ctx, workspace); err != nil {
|
||||
return models.Workspace{}, err
|
||||
}
|
||||
|
||||
// convert the created workspace to a model
|
||||
createdWorkspaceModel := models.NewWorkspaceModelFromWorkspace(workspace, workspaceKind)
|
||||
|
||||
return createdWorkspaceModel, nil
|
||||
}
|
||||
|
||||
func (r *WorkspaceRepository) DeleteWorkspace(ctx context.Context, namespace, workspaceName string) error {
|
||||
workspace := &kubefloworgv1beta1.Workspace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
Name: workspaceName,
|
||||
},
|
||||
}
|
||||
|
||||
if err := r.client.Delete(ctx, workspace); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return ErrWorkspaceNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
Reference in New Issue