880 lines
33 KiB
Go
880 lines
33 KiB
Go
/*
|
|
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"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
|
|
"github.com/julienschmidt/httprouter"
|
|
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
|
|
. "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"
|
|
|
|
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspaces"
|
|
)
|
|
|
|
var _ = Describe("Workspaces Handler", func() {
|
|
|
|
// 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 (
|
|
workspaceName1 string
|
|
workspaceKey1 types.NamespacedName
|
|
workspaceName2 string
|
|
workspaceKey2 types.NamespacedName
|
|
workspaceName3 string
|
|
workspaceKey3 types.NamespacedName
|
|
|
|
workspaceKindName string
|
|
workspaceKindKey types.NamespacedName
|
|
)
|
|
|
|
BeforeAll(func() {
|
|
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}
|
|
|
|
By("creating Namespace 1")
|
|
namespace1 := &corev1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: namespaceName1,
|
|
},
|
|
}
|
|
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())
|
|
|
|
By("creating a WorkspaceKind")
|
|
workspaceKind := NewExampleWorkspaceKind(workspaceKindName)
|
|
Expect(k8sClient.Create(ctx, workspaceKind)).To(Succeed())
|
|
|
|
By("creating Workspace 1 in Namespace 1")
|
|
workspace1 := NewExampleWorkspace(workspaceName1, namespaceName1, workspaceKindName)
|
|
Expect(k8sClient.Create(ctx, workspace1)).To(Succeed())
|
|
|
|
By("creating Workspace 2 in Namespace 1")
|
|
workspace2 := NewExampleWorkspace(workspaceName2, namespaceName1, workspaceKindName)
|
|
Expect(k8sClient.Create(ctx, workspace2)).To(Succeed())
|
|
|
|
By("creating Workspace 3 in Namespace 2")
|
|
workspace3 := NewExampleWorkspace(workspaceName3, namespaceName2, workspaceKindName)
|
|
Expect(k8sClient.Create(ctx, workspace3)).To(Succeed())
|
|
})
|
|
|
|
AfterAll(func() {
|
|
By("deleting Workspace 1 from Namespace 1")
|
|
workspace1 := &kubefloworgv1beta1.Workspace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: workspaceName1,
|
|
Namespace: namespaceName1,
|
|
},
|
|
}
|
|
Expect(k8sClient.Delete(ctx, workspace1)).To(Succeed())
|
|
|
|
By("deleting Workspace 2 from Namespace 1")
|
|
workspace2 := &kubefloworgv1beta1.Workspace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: workspaceName2,
|
|
Namespace: namespaceName1,
|
|
},
|
|
}
|
|
Expect(k8sClient.Delete(ctx, workspace2)).To(Succeed())
|
|
|
|
By("deleting Workspace 3 from Namespace 2")
|
|
workspace3 := &kubefloworgv1beta1.Workspace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: workspaceName3,
|
|
Namespace: namespaceName2,
|
|
},
|
|
}
|
|
Expect(k8sClient.Delete(ctx, workspace3)).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())
|
|
|
|
By("deleting Namespace 2")
|
|
namespace2 := &corev1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: namespaceName2,
|
|
},
|
|
}
|
|
Expect(k8sClient.Delete(ctx, namespace2)).To(Succeed())
|
|
})
|
|
|
|
It("should retrieve Workspaces from all namespaces successfully", func() {
|
|
By("creating the HTTP request")
|
|
req, err := http.NewRequest(http.MethodGet, AllWorkspacesPath, http.NoBody)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
By("setting the auth headers")
|
|
req.Header.Set(userIdHeader, adminUser)
|
|
|
|
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), descUnexpectedHTTPStatus, rr.Body.String())
|
|
|
|
By("reading the HTTP response body")
|
|
body, err := io.ReadAll(rs.Body)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
By("unmarshalling the response JSON to WorkspaceListEnvelope")
|
|
var response WorkspaceListEnvelope
|
|
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")
|
|
workspace1 := &kubefloworgv1beta1.Workspace{}
|
|
Expect(k8sClient.Get(ctx, workspaceKey1, workspace1)).To(Succeed())
|
|
workspace2 := &kubefloworgv1beta1.Workspace{}
|
|
Expect(k8sClient.Get(ctx, workspaceKey2, workspace2)).To(Succeed())
|
|
workspace3 := &kubefloworgv1beta1.Workspace{}
|
|
Expect(k8sClient.Get(ctx, workspaceKey3, workspace3)).To(Succeed())
|
|
|
|
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 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("setting the auth headers")
|
|
req.Header.Set(userIdHeader, adminUser)
|
|
|
|
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), descUnexpectedHTTPStatus, rr.Body.String())
|
|
|
|
By("reading the HTTP response body")
|
|
body, err := io.ReadAll(rs.Body)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
By("unmarshalling the response JSON to WorkspaceListEnvelope")
|
|
var response WorkspaceListEnvelope
|
|
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")
|
|
workspace1 := &kubefloworgv1beta1.Workspace{}
|
|
Expect(k8sClient.Get(ctx, workspaceKey1, workspace1)).To(Succeed())
|
|
workspace2 := &kubefloworgv1beta1.Workspace{}
|
|
Expect(k8sClient.Get(ctx, workspaceKey2, workspace2)).To(Succeed())
|
|
|
|
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, ":"+ResourceNamePathParam, workspaceName1, 1)
|
|
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
By("setting the auth headers")
|
|
req.Header.Set(userIdHeader, adminUser)
|
|
|
|
By("executing GetWorkspaceHandler")
|
|
ps := httprouter.Params{
|
|
httprouter.Param{Key: NamespacePathParam, Value: namespaceName1},
|
|
httprouter.Param{Key: ResourceNamePathParam, 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), descUnexpectedHTTPStatus, rr.Body.String())
|
|
|
|
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")
|
|
})
|
|
})
|
|
|
|
// 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 (
|
|
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}
|
|
|
|
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("setting the auth headers")
|
|
req.Header.Set(userIdHeader, adminUser)
|
|
|
|
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), descUnexpectedHTTPStatus, rr.Body.String())
|
|
|
|
By("reading the HTTP response body")
|
|
body, err := io.ReadAll(rs.Body)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
By("unmarshalling the response JSON to WorkspaceListEnvelope")
|
|
var response WorkspaceListEnvelope
|
|
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, ":"+ResourceNamePathParam, workspaceMissingWskName, 1)
|
|
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
By("setting the auth headers")
|
|
req.Header.Set(userIdHeader, adminUser)
|
|
|
|
By("executing GetWorkspaceHandler")
|
|
ps := httprouter.Params{
|
|
httprouter.Param{Key: NamespacePathParam, Value: namespaceName1},
|
|
httprouter.Param{Key: ResourceNamePathParam, 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), descUnexpectedHTTPStatus, rr.Body.String())
|
|
|
|
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() {
|
|
|
|
It("should return an empty list of Workspaces for all namespaces", func() {
|
|
By("creating the HTTP request")
|
|
req, err := http.NewRequest(http.MethodGet, AllWorkspacesPath, http.NoBody)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
By("setting the auth headers")
|
|
req.Header.Set(userIdHeader, adminUser)
|
|
|
|
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), descUnexpectedHTTPStatus, rr.Body.String())
|
|
|
|
By("reading the HTTP response body")
|
|
body, err := io.ReadAll(rs.Body)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
By("unmarshalling the response JSON to WorkspaceListEnvelope")
|
|
var response WorkspaceListEnvelope
|
|
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())
|
|
|
|
By("setting the auth headers")
|
|
req.Header.Set(userIdHeader, adminUser)
|
|
|
|
By("executing GetWorkspacesHandler")
|
|
ps := httprouter.Params{
|
|
httprouter.Param{Key: NamespacePathParam, Value: missingNamespace},
|
|
}
|
|
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), descUnexpectedHTTPStatus, rr.Body.String())
|
|
|
|
By("reading the HTTP response body")
|
|
body, err := io.ReadAll(rs.Body)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
By("unmarshalling the response JSON to WorkspaceListEnvelope")
|
|
var response WorkspaceListEnvelope
|
|
err = json.Unmarshal(body, &response)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
By("ensuring that no Workspaces were returned")
|
|
Expect(response.Data).To(BeEmpty())
|
|
})
|
|
|
|
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, ":"+ResourceNamePathParam, missingWorkspaceName, 1)
|
|
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
By("setting the auth headers")
|
|
req.Header.Set(userIdHeader, adminUser)
|
|
|
|
By("executing GetWorkspaceHandler")
|
|
ps := httprouter.Params{
|
|
httprouter.Param{Key: NamespacePathParam, Value: missingNamespace},
|
|
httprouter.Param{Key: ResourceNamePathParam, 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), descUnexpectedHTTPStatus, rr.Body.String())
|
|
})
|
|
})
|
|
|
|
// 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 = "ws-crud-ns"
|
|
|
|
var (
|
|
workspaceName string
|
|
workspaceKey types.NamespacedName
|
|
workspaceKindName string
|
|
workspaceKindKey types.NamespacedName
|
|
)
|
|
|
|
BeforeAll(func() {
|
|
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}
|
|
|
|
By("creating the Namespace")
|
|
namespaceA := &corev1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: namespaceNameCrud,
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, namespaceA)).To(Succeed())
|
|
|
|
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{
|
|
Name: workspaceKindName,
|
|
},
|
|
}
|
|
Expect(k8sClient.Delete(ctx, workspaceKind)).To(Succeed())
|
|
|
|
By("deleting the Namespace")
|
|
namespaceA := &corev1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: namespaceNameCrud,
|
|
},
|
|
}
|
|
Expect(k8sClient.Delete(ctx, namespaceA)).To(Succeed())
|
|
})
|
|
|
|
It("should create and delete a Workspace successfully", func() {
|
|
|
|
By("getting the WorkspaceKind from the Kubernetes API")
|
|
workspaceKind := &kubefloworgv1beta1.WorkspaceKind{}
|
|
Expect(k8sClient.Get(ctx, workspaceKindKey, workspaceKind)).To(Succeed())
|
|
|
|
By("defining a WorkspaceCreate model")
|
|
workspaceCreate := &models.WorkspaceCreate{
|
|
Name: workspaceName,
|
|
Kind: workspaceKindName,
|
|
Paused: false,
|
|
DeferUpdates: false,
|
|
PodTemplate: models.PodTemplateMutate{
|
|
PodMetadata: models.PodMetadataMutate{
|
|
Labels: map[string]string{
|
|
"app": "dora",
|
|
},
|
|
Annotations: map[string]string{
|
|
"app": "dora",
|
|
},
|
|
},
|
|
Volumes: models.PodVolumesMutate{
|
|
Home: ptr.To("my-home-pvc"),
|
|
Data: []models.PodVolumeMount{
|
|
{
|
|
PVCName: "my-data-pvc",
|
|
MountPath: "/data/1",
|
|
ReadOnly: false,
|
|
},
|
|
},
|
|
},
|
|
Options: models.PodTemplateOptionsMutate{
|
|
ImageConfig: "jupyterlab_scipy_180",
|
|
PodConfig: "tiny_cpu",
|
|
},
|
|
},
|
|
}
|
|
bodyEnvelope := WorkspaceCreateEnvelope{Data: workspaceCreate}
|
|
|
|
By("marshaling the WorkspaceCreate model to JSON")
|
|
bodyEnvelopeJSON, err := json.Marshal(bodyEnvelope)
|
|
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(bodyEnvelopeJSON)))
|
|
Expect(err).NotTo(HaveOccurred())
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
By("setting the auth headers")
|
|
req.Header.Set(userIdHeader, adminUser)
|
|
|
|
By("executing CreateWorkspaceHandler")
|
|
rr := httptest.NewRecorder()
|
|
ps := httprouter.Params{
|
|
httprouter.Param{
|
|
Key: NamespacePathParam,
|
|
Value: namespaceNameCrud,
|
|
},
|
|
}
|
|
a.CreateWorkspaceHandler(rr, req, ps)
|
|
rs := rr.Result()
|
|
defer rs.Body.Close()
|
|
|
|
By("verifying the HTTP response status code")
|
|
Expect(rs.StatusCode).To(Equal(http.StatusCreated), descUnexpectedHTTPStatus, rr.Body.String())
|
|
|
|
By("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.ObjectMeta.Name).To(Equal(workspaceName))
|
|
Expect(createdWorkspace.Spec.Kind).To(Equal(workspaceKindName))
|
|
Expect(createdWorkspace.Spec.Paused).To(Equal(&workspaceCreate.Paused))
|
|
Expect(createdWorkspace.Spec.DeferUpdates).To(Equal(&workspaceCreate.DeferUpdates))
|
|
Expect(createdWorkspace.Spec.PodTemplate.PodMetadata.Labels).To(Equal(workspaceCreate.PodTemplate.PodMetadata.Labels))
|
|
Expect(createdWorkspace.Spec.PodTemplate.PodMetadata.Annotations).To(Equal(workspaceCreate.PodTemplate.PodMetadata.Annotations))
|
|
Expect(createdWorkspace.Spec.PodTemplate.Volumes.Home).To(Equal(workspaceCreate.PodTemplate.Volumes.Home))
|
|
expected := []kubefloworgv1beta1.PodVolumeMount{
|
|
{
|
|
PVCName: workspaceCreate.PodTemplate.Volumes.Data[0].PVCName,
|
|
MountPath: workspaceCreate.PodTemplate.Volumes.Data[0].MountPath,
|
|
ReadOnly: &workspaceCreate.PodTemplate.Volumes.Data[0].ReadOnly,
|
|
},
|
|
}
|
|
Expect(createdWorkspace.Spec.PodTemplate.Volumes.Data).To(Equal(expected))
|
|
|
|
// TODO: Verify secrets once #240 merged
|
|
// Expect(createdWorkspace.Spec.PodTemplate.Volumes.Secrets).To(BeEmpty())
|
|
|
|
By("creating an HTTP request to delete the Workspace")
|
|
path = strings.Replace(WorkspacesByNamePath, ":"+NamespacePathParam, namespaceNameCrud, 1)
|
|
path = strings.Replace(path, ":"+ResourceNamePathParam, workspaceName, 1)
|
|
req, err = http.NewRequest(http.MethodDelete, path, http.NoBody)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
By("setting the auth headers")
|
|
req.Header.Set(userIdHeader, adminUser)
|
|
|
|
By("executing DeleteWorkspaceHandler")
|
|
rr = httptest.NewRecorder()
|
|
ps = httprouter.Params{
|
|
httprouter.Param{
|
|
Key: NamespacePathParam,
|
|
Value: namespaceNameCrud,
|
|
},
|
|
httprouter.Param{
|
|
Key: ResourceNamePathParam,
|
|
Value: workspaceName,
|
|
},
|
|
}
|
|
a.DeleteWorkspaceHandler(rr, req, ps)
|
|
rs = rr.Result()
|
|
defer rs.Body.Close()
|
|
|
|
By("verifying the HTTP response status code")
|
|
Expect(rs.StatusCode).To(Equal(http.StatusNoContent), descUnexpectedHTTPStatus, rr.Body.String())
|
|
|
|
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())
|
|
})
|
|
|
|
// TODO: Change to It when #240 merged
|
|
XIt("should create a workspace with secrets", func() {
|
|
// Create a workspace with secrets
|
|
workspace := &models.WorkspaceCreate{
|
|
Name: "test-workspace",
|
|
Kind: "test-kind",
|
|
PodTemplate: models.PodTemplateMutate{
|
|
Options: models.PodTemplateOptionsMutate{
|
|
ImageConfig: "test-image",
|
|
PodConfig: "test-config",
|
|
},
|
|
Volumes: models.PodVolumesMutate{
|
|
Data: []models.PodVolumeMount{
|
|
{
|
|
PVCName: "test-pvc",
|
|
MountPath: "/data",
|
|
},
|
|
},
|
|
Secrets: []models.PodSecretMount{
|
|
{
|
|
SecretName: "test-secret",
|
|
MountPath: "/secrets",
|
|
DefaultMode: int32(0o644),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Create the workspace using the API handler
|
|
bodyEnvelope := WorkspaceCreateEnvelope{Data: workspace}
|
|
bodyEnvelopeJSON, err := json.Marshal(bodyEnvelope)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
path := strings.Replace(WorkspacesByNamespacePath, ":"+NamespacePathParam, namespaceNameCrud, 1)
|
|
req, err := http.NewRequest(http.MethodPost, path, strings.NewReader(string(bodyEnvelopeJSON)))
|
|
Expect(err).NotTo(HaveOccurred())
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set(userIdHeader, adminUser)
|
|
|
|
rr := httptest.NewRecorder()
|
|
ps := httprouter.Params{
|
|
httprouter.Param{
|
|
Key: NamespacePathParam,
|
|
Value: namespaceNameCrud,
|
|
},
|
|
}
|
|
a.CreateWorkspaceHandler(rr, req, ps)
|
|
rs := rr.Result()
|
|
defer rs.Body.Close()
|
|
|
|
Expect(rs.StatusCode).To(Equal(http.StatusCreated), descUnexpectedHTTPStatus, rr.Body.String())
|
|
|
|
// Get the created workspace
|
|
createdWorkspace := &kubefloworgv1beta1.Workspace{}
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: workspace.Name, Namespace: namespaceNameCrud}, createdWorkspace)).To(Succeed())
|
|
|
|
// TODO: Verify the secrets are properly set once #240 merged
|
|
// Expect(createdWorkspace.Spec.PodTemplate.Volumes.Secrets).To(HaveLen(1))
|
|
// Expect(createdWorkspace.Spec.PodTemplate.Volumes.Secrets[0].SecretName).To(Equal("test-secret"))
|
|
// Expect(createdWorkspace.Spec.PodTemplate.Volumes.Secrets[0].MountPath).To(Equal("/secrets"))
|
|
// Expect(createdWorkspace.Spec.PodTemplate.Volumes.Secrets[0].DefaultMode).To(Equal(int32(0o644)))
|
|
})
|
|
|
|
// TODO: test when fail to create a Workspace when:
|
|
// - body payload invalid (missing name/kind, and/or non RCF 1123 name)
|
|
// - invalid namespace HTTP path parameter (also test for other API handlers)
|
|
})
|
|
})
|