Skip to content

Commit

Permalink
feat(config): Add config
Browse files Browse the repository at this point in the history
  • Loading branch information
Zachary Seguin committed Aug 11, 2020
1 parent a8363a5 commit e3bcdf7
Show file tree
Hide file tree
Showing 5 changed files with 356 additions and 25 deletions.
94 changes: 94 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package main

import "net/http"

type ConfigurationsConfiguration struct {
Value []string `yaml:"value" json:"value"`
ReadOnly bool `yaml:"readOnly" json:"readOnly"`
}

type SharedMemoryConfiguration struct {
Value bool `yaml:"value" json:"value"`
ReadOnly bool `yaml:"readOnly" json:"readOnly"`
}

type GPUVendorConfiguration struct {
LimitsKey string `yaml:"limitsKey" json:"limitsKey"`
UIName string `yaml:"uiName" json:"uiName"`
}

type GPUValueConfiguration struct {
Quantity string `yaml:"num" json:"num"`
Vendors []GPUVendorConfiguration `yaml:"vendors" json:"vendors"`
Vendor string `yaml:"vendor" json:"vendor"`
}

type GPUConfiguration struct {
Value GPUValueConfiguration `yaml:"value" json:"value"`
ReadOnly bool `yaml:"readOnly" json:"readOnly"`
}

type ValueConfiguration struct {
Value string `yaml:"value" json:"value"`
}

type VolumeValueConfiguration struct {
Type ValueConfiguration `yaml:"type" json:"type"`
Name ValueConfiguration `yaml:"name" json:"name"`
Size ValueConfiguration `yaml:"size" json:"size"`
MountPath ValueConfiguration `yaml:"mountPath" json:"mountPath"`
AccessModes ValueConfiguration `yaml:"accessModes" json:"accessModes"`
Class ValueConfiguration `yaml:"class" json:"class"`
}

type DataVolumesConfiguration struct {
Values []VolumeValueConfiguration `yaml:"value" json:"value"`
ReadOnly bool `yaml:"readOnly" json:"readOnly"`
}

type WorkspaceVolumeConfiguration struct {
Value VolumeValueConfiguration `yaml:"value" json:"value"`
ReadOnly bool `yaml:"readOnly" json:"readOnly"`
}

type ResourceConfiguration struct {
Value string `yaml:"value" json:"value"`
ReadOnly bool `yaml:"readOnly" json:"readOnly"`
}

type ImageConfiguration struct {
Value string `yaml:"value" json:"value"`
Options []string `yaml:"options" json:"options"`
ReadOnly bool `yaml:"readOnly" json:"readOnly"`
HideRegistry bool `yaml:"hideRegistry" json:"hideRegistry"`
HideVersion bool `yaml:"hideVersion" json:"hideVersion"`
}

type SpawnerFormDefaults struct {
Image ImageConfiguration `yaml:"image" json:"image"`
CPU ResourceConfiguration `yaml:"cpu" json:"cpu"`
Memory ResourceConfiguration `yaml:"memory" json:"memory"`
WorkspaceVolume WorkspaceVolumeConfiguration `yaml:"workspaceVolume" json:"workspaceVolume"`
DataVolumes DataVolumesConfiguration `yaml:"dataVolumes" json:"dataVolumes"`
GPUs GPUConfiguration `yaml:"gpus" json:"gpus"`
SharedMemory SharedMemoryConfiguration `yaml:"shm" json:"shm"`
Configurations ConfigurationsConfiguration `yaml:"configurations" json:"configurations"`
}

type Configuration struct {
SpawnerFormDefaults SpawnerFormDefaults `yaml:"spawnerFormDefaults" json:"spawnerFormDefaults"`
}

type configresponse struct {
APIResponse
Config SpawnerFormDefaults `json:"config"`
}

func (s *server) GetConfig(w http.ResponseWriter, r *http.Request) {
s.respond(w, r, configresponse{
APIResponse: APIResponse{
Success: true,
},
Config: s.Config.SpawnerFormDefaults,
})
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ require (
golang.org/x/text v0.3.3 // indirect
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.3.0
k8s.io/api v0.18.6
k8s.io/apimachinery v0.18.6
k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible
Expand Down
26 changes: 24 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"flag"
"io/ioutil"
"log"
"net/http"
"os"
Expand All @@ -18,6 +19,7 @@ import (
kubeflowv1alpha1listers "github.com/StatCan/kubeflow-controller/pkg/generated/listers/kubeflowcontroller/v1alpha1"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"gopkg.in/yaml.v2"
authorizationv1 "k8s.io/api/authorization/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
Expand All @@ -28,6 +30,7 @@ import (
)

var kubeconfig string
var spawnerConfigPath string
var userIDHeader string

type listers struct {
Expand All @@ -46,6 +49,8 @@ type clientsets struct {
type server struct {
mux sync.Mutex

Config Configuration

clientsets clientsets
listers listers
}
Expand All @@ -63,6 +68,24 @@ func main() {
}

flag.StringVar(&userIDHeader, "userid-header", "kubeflow-userid", "header in the request which identifies the incoming user")
flag.StringVar(&spawnerConfigPath, "spawner-config", "/etc/config/spawner_ui_config.yaml", "path to the spawner configuration file")

// Parse flags
flag.Parse()

// Setup the server
s := server{}

// Parse config
cfdata, err := ioutil.ReadFile(spawnerConfigPath)
if err != nil {
log.Fatal(err)
}

err = yaml.Unmarshal(cfdata, &s.Config)
if err != nil {
log.Fatal(err)
}

// Construct the configuration based on the provided flags.
// If no config file is provided, then the in-cluster config is used.
Expand All @@ -71,8 +94,6 @@ func main() {
log.Fatal(err)
}

s := server{}

// Generate the Kubernetes clientset
s.clientsets.kubernetes, err = kubernetes.NewForConfig(config)
if err != nil {
Expand All @@ -91,6 +112,7 @@ func main() {
router := mux.NewRouter()

// Setup route handlers
router.HandleFunc("/api/config", s.GetConfig).Methods("GET")
router.HandleFunc("/api/storageclasses/default", s.GetDefaultStorageClass).Methods("GET")

router.HandleFunc("/api/namespaces/{namespace}/notebooks", s.checkAccess(authorizationv1.SubjectAccessReview{
Expand Down
126 changes: 103 additions & 23 deletions notebooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
const DefaultServiceAccountName string = "default-editor"
const SharedMemoryVolumeName string = "dshm"
const SharedMemoryVolumePath string = "/dev/shm"
const WorkspacePath string = "/home/jovyan"

type volumetype string

Expand Down Expand Up @@ -294,6 +293,9 @@ func (s *server) NewNotebook(w http.ResponseWriter, r *http.Request) {
if req.CustomImageCheck {
image = req.CustomImage
}
if s.Config.SpawnerFormDefaults.Image.ReadOnly {
image = s.Config.SpawnerFormDefaults.Image.Value
}

// Setup the notebook
// TODO: Work with default CPU/memory limits from config
Expand All @@ -311,14 +313,8 @@ func (s *server) NewNotebook(w http.ResponseWriter, r *http.Request) {
Name: req.Name,
Image: image,
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceCPU: req.CPU,
corev1.ResourceMemory: req.Memory,
},
Requests: corev1.ResourceList{
corev1.ResourceCPU: req.CPU,
corev1.ResourceMemory: req.Memory,
},
Requests: corev1.ResourceList{},
Limits: corev1.ResourceList{},
},
},
},
Expand All @@ -327,26 +323,97 @@ func (s *server) NewNotebook(w http.ResponseWriter, r *http.Request) {
},
}

// Add workspace volume
if !req.NoWorkspace {
req.Workspace.Path = WorkspacePath
err = s.handleVolume(r.Context(), req.Workspace, &notebook)
// Resources
if s.Config.SpawnerFormDefaults.CPU.ReadOnly {
val, err := resource.ParseQuantity(s.Config.SpawnerFormDefaults.CPU.Value)
if err != nil {
s.error(w, r, err)
return
}

notebook.Spec.Template.Spec.Containers[0].Resources.Requests[corev1.ResourceCPU] = val
notebook.Spec.Template.Spec.Containers[0].Resources.Limits[corev1.ResourceCPU] = val
} else {
notebook.Spec.Template.Spec.Containers[0].Resources.Requests[corev1.ResourceCPU] = req.CPU
notebook.Spec.Template.Spec.Containers[0].Resources.Limits[corev1.ResourceCPU] = req.CPU
}

for _, volreq := range req.DataVolumes {
err = s.handleVolume(r.Context(), volreq, &notebook)
if s.Config.SpawnerFormDefaults.Memory.ReadOnly {
val, err := resource.ParseQuantity(s.Config.SpawnerFormDefaults.Memory.Value)
if err != nil {
s.error(w, r, err)
return
}

notebook.Spec.Template.Spec.Containers[0].Resources.Requests[corev1.ResourceMemory] = val
notebook.Spec.Template.Spec.Containers[0].Resources.Limits[corev1.ResourceMemory] = val
} else {
notebook.Spec.Template.Spec.Containers[0].Resources.Requests[corev1.ResourceMemory] = req.Memory
notebook.Spec.Template.Spec.Containers[0].Resources.Limits[corev1.ResourceMemory] = req.Memory
}

// Add workspace volume
if s.Config.SpawnerFormDefaults.WorkspaceVolume.ReadOnly {
size, err := resource.ParseQuantity(s.Config.SpawnerFormDefaults.WorkspaceVolume.Value.Size.Value)
if err != nil {
s.error(w, r, err)
return
}

workspaceVol := volumerequest{
Name: s.Config.SpawnerFormDefaults.WorkspaceVolume.Value.Name.Value,
Size: size,
Path: s.Config.SpawnerFormDefaults.WorkspaceVolume.Value.MountPath.Value,
Mode: corev1.PersistentVolumeAccessMode(s.Config.SpawnerFormDefaults.WorkspaceVolume.Value.AccessModes.Value),
Class: s.Config.SpawnerFormDefaults.WorkspaceVolume.Value.Class.Value,
}
err = s.handleVolume(r.Context(), workspaceVol, &notebook)
if err != nil {
s.error(w, r, err)
return
}
} else if !req.NoWorkspace {
req.Workspace.Path = s.Config.SpawnerFormDefaults.WorkspaceVolume.Value.MountPath.Value
err = s.handleVolume(r.Context(), req.Workspace, &notebook)
if err != nil {
s.error(w, r, err)
return
}
}

if s.Config.SpawnerFormDefaults.DataVolumes.ReadOnly {
for _, volreq := range s.Config.SpawnerFormDefaults.DataVolumes.Values {
size, err := resource.ParseQuantity(s.Config.SpawnerFormDefaults.WorkspaceVolume.Value.Size.Value)
if err != nil {
s.error(w, r, err)
return
}

vol := volumerequest{
Name: volreq.Name.Value,
Size: size,
Path: volreq.MountPath.Value,
Mode: corev1.PersistentVolumeAccessMode(volreq.AccessModes.Value),
Class: volreq.Class.Value,
}
err = s.handleVolume(r.Context(), vol, &notebook)
if err != nil {
s.error(w, r, err)
return
}
}
} else {
for _, volreq := range req.DataVolumes {
err = s.handleVolume(r.Context(), volreq, &notebook)
if err != nil {
s.error(w, r, err)
return
}
}
}

// Add shared memory, if enabled
if req.EnableSharedMemory {
if (s.Config.SpawnerFormDefaults.SharedMemory.ReadOnly && s.Config.SpawnerFormDefaults.SharedMemory.Value) || (!s.Config.SpawnerFormDefaults.SharedMemory.ReadOnly && req.EnableSharedMemory) {
notebook.Spec.Template.Spec.Volumes = append(notebook.Spec.Template.Spec.Volumes, corev1.Volume{
Name: SharedMemoryVolumeName,
VolumeSource: corev1.VolumeSource{
Expand All @@ -363,15 +430,28 @@ func (s *server) NewNotebook(w http.ResponseWriter, r *http.Request) {
}

// Add GPU
if req.GPUs.Quantity != "none" {
qty, err := resource.ParseQuantity(req.GPUs.Quantity)
if err != nil {
s.error(w, r, err)
return
if s.Config.SpawnerFormDefaults.GPUs.ReadOnly {
if s.Config.SpawnerFormDefaults.GPUs.Value.Quantity != "none" {
qty, err := resource.ParseQuantity(s.Config.SpawnerFormDefaults.GPUs.Value.Quantity)
if err != nil {
s.error(w, r, err)
return
}

notebook.Spec.Template.Spec.Containers[0].Resources.Requests[corev1.ResourceName(s.Config.SpawnerFormDefaults.GPUs.Value.Vendor)] = qty
notebook.Spec.Template.Spec.Containers[0].Resources.Limits[corev1.ResourceName(s.Config.SpawnerFormDefaults.GPUs.Value.Vendor)] = qty
}
} else {
if req.GPUs.Quantity != "none" {
qty, err := resource.ParseQuantity(req.GPUs.Quantity)
if err != nil {
s.error(w, r, err)
return
}

notebook.Spec.Template.Spec.Containers[0].Resources.Requests[corev1.ResourceName(req.GPUs.Vendor)] = qty
notebook.Spec.Template.Spec.Containers[0].Resources.Limits[corev1.ResourceName(req.GPUs.Vendor)] = qty
notebook.Spec.Template.Spec.Containers[0].Resources.Requests[corev1.ResourceName(req.GPUs.Vendor)] = qty
notebook.Spec.Template.Spec.Containers[0].Resources.Limits[corev1.ResourceName(req.GPUs.Vendor)] = qty
}
}

log.Printf("creating notebook %q for %q", notebook.ObjectMeta.Name, namespace)
Expand Down
Loading

0 comments on commit e3bcdf7

Please sign in to comment.