Skip to content

Commit

Permalink
add level and sandboxname to course schema
Browse files Browse the repository at this point in the history
  • Loading branch information
simonycj committed Jul 9, 2024
1 parent cdfed7e commit 885b3c1
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 15 deletions.
1 change: 1 addition & 0 deletions deploy/install-open-hydra-keystone.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ apiVersion: v1
data:
plugins: |
{
"defaultSandbox": "xedu",
"sandboxes": {
"xedu": {
"cpuImageName": "registry.cn-shanghai.aliyuncs.com/openhydra/jupyter:Python-3.8.18-dual-lan",
Expand Down
1 change: 1 addition & 0 deletions deploy/install-open-hydra.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ apiVersion: v1
data:
plugins: |
{
"defaultSandbox": "xedu",
"sandboxes": {
"xedu": {
"cpuImageName": "registry.cn-shanghai.aliyuncs.com/openhydra/jupyter:Python-3.8.18-dual-lan",
Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/open-hydra-api/course/core/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type CourseSpec struct {
CreatedBy string `json:"createdBy,omitempty"`
Description string `json:"description,omitempty"`
LastUpdate metav1.Time `json:"lastUpdate"`
Level int `json:"level,omitempty"`
SandboxName string `json:"sandboxName,omitempty"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
Expand Down
42 changes: 38 additions & 4 deletions pkg/generated/apis/openapi/openapi_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pkg/open-hydra/apis/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@ type Sandbox struct {

// +k8s:openapi-gen=true
type PluginList struct {
Sandboxes map[string]Sandbox `json:"sandboxes"`
DefaultSandbox string `json:"defaultSandbox"`
Sandboxes map[string]Sandbox `json:"sandboxes"`
}
40 changes: 40 additions & 0 deletions pkg/open-hydra/course-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
"os"
"path/filepath"
"strconv"
"strings"

"k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -79,11 +80,48 @@ func (builder *OpenHydraRouteBuilder) CourseCreateRouteHandler(request *restful.
description := request.Request.PostFormValue("description")
createdBy := request.Request.PostFormValue("createdBy")
name := request.Request.PostFormValue("name")
levelRaw := request.Request.PostFormValue("level")
sandboxName := request.Request.PostFormValue("sandboxName")
if name == "" {
writeHttpResponseAndLogError(response, http.StatusBadRequest, "Course name is empty")
return
}

// we need to get config map openhydra-plugin first
// TODO: we should use informer to cache config map instead of query api-server directly for performance
pluginConfigMap, err := builder.k8sHelper.GetMap("openhydra-plugin", builder.Config.OpenHydraNamespace, builder.kubeClient)
if err != nil {
writeHttpResponseAndLogError(response, http.StatusInternalServerError, fmt.Sprintf("Failed to get configmap: %v", err))
return
}

// parse to plugin list
plugins, err := ParseJsonToPluginList(pluginConfigMap.Data["plugins"])
if err != nil {
writeHttpResponseAndLogError(response, http.StatusInternalServerError, fmt.Sprintf("Failed to unmarshal json: %v", err))
return
}

if sandboxName == "" {
sandboxName = plugins.DefaultSandbox
} else {
if _, found := plugins.Sandboxes[sandboxName]; !found {
writeHttpResponseAndLogError(response, http.StatusBadRequest, fmt.Sprintf("Sandbox %s not found", sandboxName))
return
}
}

level := 0
if levelRaw != "" {
// try parse to int
level, err = strconv.Atoi(levelRaw)
if err != nil {
writeHttpResponseAndLogError(response, http.StatusBadRequest,
fmt.Sprintf("Failed to parse level: %v", err.Error()))
return
}
}

file, fileHeader, err := request.Request.FormFile("file")
if err != nil {
writeHttpResponseAndLogError(response, http.StatusBadRequest,
Expand Down Expand Up @@ -125,6 +163,8 @@ func (builder *OpenHydraRouteBuilder) CourseCreateRouteHandler(request *restful.
Spec: xCourseV1.CourseSpec{
Description: description,
CreatedBy: createdBy,
Level: level,
SandboxName: sandboxName,
},
}

Expand Down
4 changes: 3 additions & 1 deletion pkg/open-hydra/k8s/faker.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ func (f *Fake) DeleteUserPod(label, namespace string, client *kubernetes.Clients
func (f *Fake) GetMap(name, namespace string, client *kubernetes.Clientset) (*coreV1.ConfigMap, error) {
return &coreV1.ConfigMap{
Data: map[string]string{
"plugins": `{"sandboxes":{
"plugins": `{
"defaultSandbox": "test",
"sandboxes":{
"test": {
"display_title": "test",
"cpuImageName": "test",
Expand Down
103 changes: 94 additions & 9 deletions pkg/open-hydra/open-hydra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,10 @@ var _ = Describe("open-hydra-server authorization test", func() {
}
container.Add(builder.RootWS)
}
var uploadResource = func(url, testZipBaseDir string) {
var uploadResource = func(url, testZipBaseDir string, extraData map[string]string, statusExpected int) {
if statusExpected == 0 {
statusExpected = http.StatusOK
}
err := util.CreateDirIfNotExists("/tmp/test")
Expect(err).To(BeNil())
err = util.CreateDirIfNotExists("/tmp/test/test1")
Expand All @@ -762,14 +765,17 @@ var _ = Describe("open-hydra-server authorization test", func() {
Expect(err).To(BeNil())
err = util.ZipDir("/tmp/test", "/tmp/test.zip")
Expect(err).To(BeNil())
bodyTxt := map[string]string{}
bodyTxt := extraData
bodyTxt["name"] = "unit-test"
bodyTxt["description"] = "unit-test"
body, contentType, err := createMultiPartBody(bodyTxt, "/tmp/test.zip")
//body, contentType, err := createMultiPartBody(bodyTxt, "/tmp/ds1.zip")
Expect(err).To(BeNil())
_, r2 := callApi(http.MethodPost, url, createTokenValue(teacher, map[string]string{"Content-Type": contentType}), body)
Expect(r2.Code).To(Equal(http.StatusCreated))
Expect(r2.Code).To(Equal(statusExpected))
if statusExpected != http.StatusOK {
return
}
targetPath := path.Join(testZipBaseDir, "unit-test", "test", "test1", "test.txt")
data, err := util.ReadTxtFile(targetPath)
Expect(err).To(BeNil())
Expand Down Expand Up @@ -1108,7 +1114,7 @@ var _ = Describe("open-hydra-server authorization test", func() {
})

It("create dataset by teacher should be ok", func() {
uploadResource(openHydraDatasetsURL, openHydraConfig.PublicDatasetBasePath)
uploadResource(openHydraDatasetsURL, openHydraConfig.PublicDatasetBasePath, map[string]string{}, http.StatusCreated)

// list it
_, r2 := callApi(http.MethodGet, openHydraDatasetsURL, createTokenValue(teacher, nil), nil)
Expand Down Expand Up @@ -1136,7 +1142,7 @@ var _ = Describe("open-hydra-server authorization test", func() {
})

It("dataset will reject students", func() {
uploadResource(openHydraDatasetsURL, openHydraConfig.PublicDatasetBasePath)
uploadResource(openHydraDatasetsURL, openHydraConfig.PublicDatasetBasePath, map[string]string{}, http.StatusCreated)
_, r2 := callApi(http.MethodGet, openHydraDatasetsURL, createTokenValue(student, nil), nil)
Expect(r2.Code).To(Equal(http.StatusForbidden))
_, r2 = callApi(http.MethodGet, openHydraDatasetsURL+"/unit-test", createTokenValue(student, nil), nil)
Expand All @@ -1146,7 +1152,7 @@ var _ = Describe("open-hydra-server authorization test", func() {
})

It("dataset delete by teacher should be ok", func() {
uploadResource(openHydraDatasetsURL, openHydraConfig.PublicDatasetBasePath)
uploadResource(openHydraDatasetsURL, openHydraConfig.PublicDatasetBasePath, map[string]string{}, http.StatusCreated)
_, r2 := callApi(http.MethodDelete, openHydraDatasetsURL+"/unit-test", createTokenValue(teacher, nil), nil)
Expect(r2.Code).To(Equal(http.StatusOK))
_, err := os.Stat(path.Join(openHydraConfig.PublicDatasetBasePath, "unit-test"))
Expand All @@ -1164,7 +1170,84 @@ var _ = Describe("open-hydra-server authorization test", func() {
})

It("create course by teacher should be ok", func() {
uploadResource(openHydraCoursesURL, openHydraConfig.PublicCourseBasePath)
uploadResource(openHydraCoursesURL, openHydraConfig.PublicCourseBasePath, map[string]string{}, http.StatusCreated)

// list it
_, r2 := callApi(http.MethodGet, openHydraCoursesURL, createTokenValue(teacher, nil), nil)
Expect(r2.Code).To(Equal(http.StatusOK))
var target xCourseV1.CourseList
result, err := io.ReadAll(r2.Body)
Expect(err).To(BeNil())
err = json.Unmarshal(result, &target)
Expect(err).To(BeNil())
Expect(len(target.Items)).To(Equal(1))

// get it
_, r2 = callApi(http.MethodGet, openHydraCoursesURL+"/unit-test", createTokenValue(teacher, nil), nil)
Expect(r2.Code).To(Equal(http.StatusOK))
var target2 xCourseV1.Course
result, err = io.ReadAll(r2.Body)
Expect(err).To(BeNil())
err = json.Unmarshal(result, &target2)
Expect(err).To(BeNil())
Expect(target2.Name).To(Equal("unit-test"))
Expect(target2.Spec.Description).To(Equal("unit-test"))

util.DeleteDirs("/tmp/test")
util.DeleteDirs(path.Join(openHydraConfig.PublicCourseBasePath, "unit-test"))
})

It("create course by teacher failed due to level not parsable", func() {
uploadResource(openHydraCoursesURL, openHydraConfig.PublicCourseBasePath, map[string]string{
"level": "funny",
}, http.StatusBadRequest)
util.DeleteDirs("/tmp/test")
util.DeleteDirs(path.Join(openHydraConfig.PublicCourseBasePath, "unit-test"))
})

It("create course by teacher failed due to sandbox not pre-config well", func() {
uploadResource(openHydraCoursesURL, openHydraConfig.PublicCourseBasePath, map[string]string{
"sandboxName": "sandbox-not-exists",
}, http.StatusBadRequest)
util.DeleteDirs("/tmp/test")
util.DeleteDirs(path.Join(openHydraConfig.PublicCourseBasePath, "unit-test"))
})

It("create course by teacher should be ok with level and sandbox proper returned", func() {
uploadResource(openHydraCoursesURL, openHydraConfig.PublicCourseBasePath, map[string]string{
"level": "1",
"sandboxName": "jupyter-lab",
}, http.StatusCreated)

// list it
_, r2 := callApi(http.MethodGet, openHydraCoursesURL, createTokenValue(teacher, nil), nil)
Expect(r2.Code).To(Equal(http.StatusOK))
var target xCourseV1.CourseList
result, err := io.ReadAll(r2.Body)
Expect(err).To(BeNil())
err = json.Unmarshal(result, &target)
Expect(err).To(BeNil())
Expect(len(target.Items)).To(Equal(1))

// get it
_, r2 = callApi(http.MethodGet, openHydraCoursesURL+"/unit-test", createTokenValue(teacher, nil), nil)
Expect(r2.Code).To(Equal(http.StatusOK))
var target2 xCourseV1.Course
result, err = io.ReadAll(r2.Body)
Expect(err).To(BeNil())
err = json.Unmarshal(result, &target2)
Expect(err).To(BeNil())
Expect(target2.Name).To(Equal("unit-test"))
Expect(target2.Spec.Description).To(Equal("unit-test"))
Expect(target2.Spec.Level).To(Equal(1))
Expect(target2.Spec.SandboxName).To(Equal("jupyter-lab"))

util.DeleteDirs("/tmp/test")
util.DeleteDirs(path.Join(openHydraConfig.PublicCourseBasePath, "unit-test"))
})

It("create course by teacher should be ok with level and sandbox default value is returned", func() {
uploadResource(openHydraCoursesURL, openHydraConfig.PublicCourseBasePath, map[string]string{}, http.StatusCreated)

// list it
_, r2 := callApi(http.MethodGet, openHydraCoursesURL, createTokenValue(teacher, nil), nil)
Expand All @@ -1186,13 +1269,15 @@ var _ = Describe("open-hydra-server authorization test", func() {
Expect(err).To(BeNil())
Expect(target2.Name).To(Equal("unit-test"))
Expect(target2.Spec.Description).To(Equal("unit-test"))
Expect(target2.Spec.Level).To(Equal(0))
Expect(target2.Spec.SandboxName).To(Equal("test"))

util.DeleteDirs("/tmp/test")
util.DeleteDirs(path.Join(openHydraConfig.PublicCourseBasePath, "unit-test"))
})

It("course will reject students", func() {
uploadResource(openHydraCoursesURL, openHydraConfig.PublicCourseBasePath)
uploadResource(openHydraCoursesURL, openHydraConfig.PublicCourseBasePath, map[string]string{}, http.StatusCreated)
_, r2 := callApi(http.MethodGet, openHydraCoursesURL, createTokenValue(student, nil), nil)
Expect(r2.Code).To(Equal(http.StatusForbidden))
_, r2 = callApi(http.MethodGet, openHydraCoursesURL+"/unit-test", createTokenValue(student, nil), nil)
Expand All @@ -1202,7 +1287,7 @@ var _ = Describe("open-hydra-server authorization test", func() {
})

It("course delete by teacher should be ok", func() {
uploadResource(openHydraCoursesURL, openHydraConfig.PublicCourseBasePath)
uploadResource(openHydraCoursesURL, openHydraConfig.PublicCourseBasePath, map[string]string{}, http.StatusCreated)
_, r2 := callApi(http.MethodDelete, openHydraCoursesURL+"/unit-test", createTokenValue(teacher, nil), nil)
Expect(r2.Code).To(Equal(http.StatusOK))
_, err := os.Stat(path.Join(openHydraConfig.PublicCourseBasePath, "unit-test"))
Expand Down

0 comments on commit 885b3c1

Please sign in to comment.