Skip to content

Commit d36aff0

Browse files
author
Zachary Seguin
committed
feat(notebooks): Add initial support for creating and delete notebooks
1 parent 7f366f8 commit d36aff0

File tree

2 files changed

+212
-0
lines changed

2 files changed

+212
-0
lines changed

main.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ func main() {
8484

8585
// Setup route handlers
8686
router.HandleFunc("/api/storageclasses/default", s.GetDefaultStorageClass).Methods("GET")
87+
8788
router.HandleFunc("/api/namespaces/{namespace}/notebooks", s.checkAccess(authorizationv1.SubjectAccessReview{
8889
Spec: authorizationv1.SubjectAccessReviewSpec{
8990
ResourceAttributes: &authorizationv1.ResourceAttributes{
@@ -94,6 +95,27 @@ func main() {
9495
},
9596
},
9697
}, s.GetNotebooks)).Methods("GET")
98+
router.HandleFunc("/api/namespaces/{namespace}/notebooks", s.checkAccess(authorizationv1.SubjectAccessReview{
99+
Spec: authorizationv1.SubjectAccessReviewSpec{
100+
ResourceAttributes: &authorizationv1.ResourceAttributes{
101+
Group: notebooksv1.GroupVersion.Group,
102+
Verb: "create",
103+
Resource: "notebooks",
104+
Version: notebooksv1.GroupVersion.Version,
105+
},
106+
},
107+
}, s.NewNotebook)).Headers("Content-Type", "application/json").Methods("POST")
108+
router.HandleFunc("/api/namespaces/{namespace}/notebooks/{notebook}", s.checkAccess(authorizationv1.SubjectAccessReview{
109+
Spec: authorizationv1.SubjectAccessReviewSpec{
110+
ResourceAttributes: &authorizationv1.ResourceAttributes{
111+
Group: notebooksv1.GroupVersion.Group,
112+
Verb: "delete",
113+
Resource: "notebooks",
114+
Version: notebooksv1.GroupVersion.Version,
115+
},
116+
},
117+
}, s.DeleteNotebook)).Methods("DELETE")
118+
97119
router.HandleFunc("/api/namespaces/{namespace}/pvcs", s.checkAccess(authorizationv1.SubjectAccessReview{
98120
Spec: authorizationv1.SubjectAccessReviewSpec{
99121
ResourceAttributes: &authorizationv1.ResourceAttributes{

notebooks.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package main
22

33
import (
4+
"encoding/json"
45
"fmt"
6+
"io/ioutil"
57
"log"
68
"net/http"
79
"sort"
@@ -15,6 +17,43 @@ import (
1517
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1618
)
1719

20+
const DefaultServiceAccountName string = "default-editor"
21+
const SharedMemoryVolumeName string = "dshm"
22+
const SharedMemoryVolumePath string = "/dev/shm"
23+
const WorkspacePath string = "/home/jovyan"
24+
25+
type volumetype string
26+
27+
const (
28+
VolumeTypeExisting volumetype = "Existing"
29+
VolumeTypeNew volumetype = "New"
30+
)
31+
32+
type volumerequest struct {
33+
Type volumetype `json:"type"`
34+
Name string `json:"name"`
35+
TemplatedName string `json:"templatedName"`
36+
Class string `json:"class"`
37+
ExtraFields map[string]interface{} `json:"extraFields"`
38+
Path string `json:"path"`
39+
}
40+
41+
type newnotebookrequest struct {
42+
Name string `json:"name"`
43+
Namespace string `json:"namespace"`
44+
Image string `json:"image"`
45+
CustomImage string `json:"customImage"`
46+
CustomImageCheck bool `json:"customImageCheck"`
47+
CPU resource.Quantity `json:"cpu"`
48+
Memory resource.Quantity `json:"memory"`
49+
// TODO: GPU
50+
NoWorkspace bool `json:"noWorkspace"`
51+
Workspace volumerequest `json:"workspace"`
52+
DataVolumes []volumerequest `json:"datavols"`
53+
EnableSharedMemory bool `json:"shm"`
54+
Configurations []string `json:"configurations"`
55+
}
56+
1857
type notebookresponse struct {
1958
Age string `json:"age"`
2059
CPU resource.Quantity `json:"cpu"`
@@ -186,3 +225,154 @@ func (s *server) GetNotebooks(w http.ResponseWriter, r *http.Request) {
186225

187226
s.respond(w, r, resp)
188227
}
228+
229+
func (s *server) handleVolume(req volumerequest, notebook *notebooksv1.Notebook) error {
230+
if req.Type == VolumeTypeExisting {
231+
notebook.Spec.Template.Spec.Volumes = append(notebook.Spec.Template.Spec.Volumes, corev1.Volume{
232+
Name: req.Name,
233+
VolumeSource: corev1.VolumeSource{
234+
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
235+
ClaimName: req.Name,
236+
},
237+
},
238+
})
239+
240+
notebook.Spec.Template.Spec.Containers[0].VolumeMounts = append(notebook.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
241+
Name: req.Name,
242+
MountPath: req.Path,
243+
})
244+
} else if req.Type == VolumeTypeNew {
245+
return fmt.Errorf("unsupported volume type %q", req.Type)
246+
} else {
247+
return fmt.Errorf("unknown volume type %q", req.Type)
248+
}
249+
250+
return nil
251+
}
252+
253+
func (s *server) NewNotebook(w http.ResponseWriter, r *http.Request) {
254+
vars := mux.Vars(r)
255+
namespace := vars["namespace"]
256+
257+
// Read the incoming notebook
258+
body, err := ioutil.ReadAll(r.Body)
259+
if err != nil {
260+
s.error(w, r, err)
261+
return
262+
}
263+
defer r.Body.Close()
264+
265+
var req newnotebookrequest
266+
err = json.Unmarshal(body, &req)
267+
if err != nil {
268+
s.error(w, r, err)
269+
return
270+
}
271+
272+
image := req.Image
273+
if req.CustomImageCheck {
274+
image = req.CustomImage
275+
}
276+
277+
// Setup the notebook
278+
// TODO: Work with default CPU/memory limits from config
279+
// TODO: Add GPU support
280+
notebook := notebooksv1.Notebook{
281+
ObjectMeta: v1.ObjectMeta{
282+
Name: req.Name,
283+
Namespace: namespace,
284+
},
285+
Spec: notebooksv1.NotebookSpec{
286+
Template: notebooksv1.NotebookTemplateSpec{
287+
Spec: corev1.PodSpec{
288+
ServiceAccountName: DefaultServiceAccountName,
289+
Containers: []corev1.Container{
290+
corev1.Container{
291+
Name: req.Name,
292+
Image: image,
293+
Resources: corev1.ResourceRequirements{
294+
Limits: corev1.ResourceList{
295+
corev1.ResourceCPU: req.CPU,
296+
corev1.ResourceMemory: req.Memory,
297+
},
298+
Requests: corev1.ResourceList{
299+
corev1.ResourceCPU: req.CPU,
300+
corev1.ResourceMemory: req.Memory,
301+
},
302+
},
303+
},
304+
},
305+
},
306+
},
307+
},
308+
}
309+
310+
// Add workspace volume
311+
if !req.NoWorkspace {
312+
req.Workspace.Path = WorkspacePath
313+
err = s.handleVolume(req.Workspace, &notebook)
314+
if err != nil {
315+
s.error(w, r, err)
316+
return
317+
}
318+
}
319+
320+
for _, volreq := range req.DataVolumes {
321+
err = s.handleVolume(volreq, &notebook)
322+
if err != nil {
323+
s.error(w, r, err)
324+
return
325+
}
326+
}
327+
328+
// Add shared memory, if enabled
329+
if req.EnableSharedMemory {
330+
notebook.Spec.Template.Spec.Volumes = append(notebook.Spec.Template.Spec.Volumes, corev1.Volume{
331+
Name: SharedMemoryVolumeName,
332+
VolumeSource: corev1.VolumeSource{
333+
EmptyDir: &corev1.EmptyDirVolumeSource{
334+
Medium: corev1.StorageMediumMemory,
335+
},
336+
},
337+
})
338+
339+
notebook.Spec.Template.Spec.Containers[0].VolumeMounts = append(notebook.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
340+
Name: SharedMemoryVolumeName,
341+
MountPath: SharedMemoryVolumePath,
342+
})
343+
}
344+
345+
log.Printf("creating notebook %q for %q", notebook.ObjectMeta.Name, namespace)
346+
347+
// Submit the notebook to the API server
348+
_, err = s.clientsets.notebooks.V1().Notebooks(namespace).Create(r.Context(), &notebook)
349+
if err != nil {
350+
s.error(w, r, err)
351+
return
352+
}
353+
354+
s.respond(w, r, APIResponse{
355+
Success: true,
356+
})
357+
}
358+
359+
func (s *server) DeleteNotebook(w http.ResponseWriter, r *http.Request) {
360+
vars := mux.Vars(r)
361+
namespace := vars["namespace"]
362+
notebook := vars["notebook"]
363+
364+
log.Printf("deleting notebook %q for %q", notebook, namespace)
365+
366+
propagation := v1.DeletePropagationForeground
367+
err := s.clientsets.notebooks.V1().Notebooks(namespace).Delete(r.Context(), notebook, &v1.DeleteOptions{
368+
PropagationPolicy: &propagation,
369+
})
370+
if err != nil {
371+
s.error(w, r, err)
372+
return
373+
}
374+
375+
s.respond(w, r, APIResponse{
376+
Success: true,
377+
})
378+
}

0 commit comments

Comments
 (0)