Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't create PVCs if no default StorageClass is set #2679

Merged
merged 13 commits into from
Mar 20, 2019
60 changes: 29 additions & 31 deletions components/jupyter-web-app/default/kubeflow/jupyter/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@
from kubernetes.client.rest import ApiException
from kubeflow.jupyter import app
from kubeflow.jupyter.server import parse_error, \
get_namespaces, \
get_notebooks, \
delete_notebook, \
create_notebook, \
create_pvc
get_namespaces, \
get_notebooks, \
get_default_storageclass, \
delete_notebook, \
create_notebook, \
create_datavol_pvc, \
create_workspace_pvc
from kubeflow.jupyter.utils import create_notebook_template, \
create_pvc_template, \
set_notebook_names, \
set_notebook_image, \
set_notebook_cpu_ram, \
add_notebook_volume, \
spawner_ui_config
set_notebook_names, \
set_notebook_image, \
set_notebook_cpu_ram, \
add_notebook_volume, \
spawner_ui_config, \
create_logger

logger = create_logger(__name__)


# Helper function for getting the prefix of the webapp
Expand Down Expand Up @@ -46,15 +50,8 @@ def post_notebook_route():

# Workspacae Volume
if body["ws_type"] == "New":
pvc = create_pvc_template()
pvc['metadata']['name'] = body['ws_name']
pvc['metadata']['namespace'] = body['ns']
pvc['spec']['accessModes'].append(body['ws_access_modes'])
pvc['spec']['resources']['requests']['storage'] = \
body['ws_size'] + 'Gi'

try:
create_pvc(pvc)
create_workspace_pvc(body)
except ApiException as e:
data["success"] = False
data["log"] = parse_error(e)
Expand All @@ -79,22 +76,14 @@ def post_notebook_route():

# Create a PVC if its a new Data Volume
if body["vol_type" + i] == "New":
size = body['vol_size' + i] + 'Gi'
mode = body['vol_access_modes' + i]
pvc = create_pvc_template()

pvc['metadata']['name'] = pvc_nm
pvc['metadata']['namespace'] = body['ns']
pvc['spec']['accessModes'].append(mode)
pvc['spec']['resources']['requests']['storage'] = size

try:
create_pvc(pvc)
create_datavol_pvc(data, i)
except ApiException as e:
data["success"] = False
data["log"] = parse_error(e)
return jsonify(data)

# Create the Data Volume in the Pod
add_notebook_volume(notebook, vol_nm, pvc_nm, mnt)
counter += 1

Expand Down Expand Up @@ -128,9 +117,17 @@ def add_notebook_route():
else:
ns = "kubeflow"

is_default = False
try:
if get_default_storageclass() != "":
is_default = True
except ApiException as e:
logger.warning("Can't list storageclasses: %s" % parse_error(e))

form_defaults = spawner_ui_config("notebook")
return render_template(
'add_notebook.html', prefix=prefix(), ns=ns, form_defaults=form_defaults)
'add_notebook.html', prefix=prefix(), ns=ns, form_defaults=form_defaults,
default_storage_class=is_default)


@app.route("/delete-notebook", methods=['GET', 'POST'])
Expand Down Expand Up @@ -174,8 +171,9 @@ def notebooks_route():
# Get the namespaces the token can see
try:
nmsps = get_namespaces()
except ApiException:
except ApiException as e:
nmsps = [base_ns]
logger.warning("Can't list namespaces: %s" % parse_error(e))

return render_template(
'notebooks.html', prefix=prefix(), title='Notebooks', namespaces=nmsps)
75 changes: 74 additions & 1 deletion components/jupyter-web-app/default/kubeflow/jupyter/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
import json
from kubernetes import client, config
from kubernetes.config import ConfigException
from kubeflow.jupyter.utils import create_logger

logger = create_logger(__name__)

try:
# Load configuration inside the Pod
Expand All @@ -13,6 +16,7 @@
# Create the Apis
v1_core = client.CoreV1Api()
custom_api = client.CustomObjectsApi()
storage_api = client.StorageV1Api()


def parse_error(e):
Expand All @@ -26,10 +30,79 @@ def parse_error(e):
return err


def create_workspace_pvc(body):
"""If the type is New, then create a new PVC, else use an existing one"""
if body["ws_type"] == "New":
pvc = client.V1PersistentVolumeClaim(
metadata=client.V1ObjectMeta(
name=body['ws_name'],
namespace=body['ns']
),
spec=client.V1PersistentVolumeClaimSpec(
access_modes=[body['ws_access_modes']],
resources=client.V1ResourceRequirements(
requests={
'storage': body['ws_size'] + 'Gi'
}
)
)
)

create_pvc(pvc)

return


def create_datavol_pvc(body, i):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally I think there would be some kind of definition for the shape of the body dict being passed in, but since I have almost no context of looking at this I'm not going to be picky.

pvc_nm = body['vol_name' + i]

# Create a PVC if its a new Data Volume
if body["vol_type" + i] == "New":
size = body['vol_size' + i] + 'Gi'
mode = body['vol_access_modes' + i]

pvc = client.V1PersistentVolumeClaim(
metadata=client.V1ObjectMeta(
name=pvc_nm,
namespace=body['ns']
),
spec=client.V1PersistentVolumeClaimSpec(
access_modes=[mode],
resources=client.V1ResourceRequirements(
requests={
'storage': size
}
)
)
)

create_pvc(pvc)

return


def get_secret(nm, ns):
return v1_core.read_namespaced_secret(nm, ns)


def get_default_storageclass():
strg_classes = storage_api.list_storage_class().items
for strgclss in strg_classes:
annotations = strgclss.metadata.annotations
# List of possible annotations
keys = []
keys.append("storageclass.kubernetes.io/is-default-class")
keys.append("storageclass.beta.kubernetes.io/is-default-class") # GKE

for key in keys:
is_default = annotations.get(key, False)
if is_default:
return strgclss.metadata.name

# No StorageClass is default
return ""


def get_namespaces():
nmsps = v1_core.list_namespace()
return [ns.metadata.name for ns in nmsps.items]
Expand Down Expand Up @@ -60,5 +133,5 @@ def create_notebook(body):


def create_pvc(body):
ns = body['metadata']['namespace']
ns = body.metadata.namespace
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in this case body has actually changed from a dict to an object. Again, having no context it may be helpful to have some kind of comment or change the name of the param to reflect that it's not a deserialized JSON response as a dict but is not a typed object.

return v1_core.create_namespaced_persistent_volume_claim(ns, body)
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@
display: none;
}

.round {
border-radius: 4px;
}

.round-top {
border-top-right-radius: 4px;
border-top-left-radius: 4px;
}

.my-table {
width: 75%;
top: 10px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,14 @@ function setDefaultFormValues() {
}
}
}
// If default StorageClass is not set, only allow None as option
if (default_storage_class === "False") {
$('#ws_type')
.empty()
.append($('<option/>').attr({selected: true, value: 'None'}).text('None'))
.append($('<option/>').attr({value: 'Existing'}).text('Existing'))
$('#ws_type').val('None')
}
$('#ws_type').trigger('change');

// Name
Expand Down Expand Up @@ -474,9 +482,11 @@ function addVolume() {
for: "vol_type" + counter
}).text("Type")

volumeType
.append($('<option/>').attr({selected: true, value: 'New'}).text('New'))
.append($('<option/>').attr({value: 'Existing'}).text('Existing'));
// If no default StorageClass exists, don't create New Data Volumes
if (default_storage_class !== "False") {
volumeType.append($('<option/>').attr({selected: true, value: 'New'}).text('New'))
}
volumeType.append($('<option/>').attr({value: 'Existing'}).text('Existing'))

volumeTypeTextField.append(volumeType)
volumeTypeTextField.append(volumeTypeLabel)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
var username = "notebook"
var namespace = "{{ ns }}"
var existingPVCs = ""
var default_storage_class = "{{ default_storage_class }}"
</script>
<script src="{{ prefix + url_for('static', filename='js/add_notebook.js') }}"></script>
{% endblock %}
Expand All @@ -16,10 +17,10 @@
<div class="mdl-grid center-items">
<div class="mdl-cell mdl-cell--6-col">

<div class="mdl-card mdl-shadow--8dp wide">
<div class="mdl-card mdl-shadow--8dp wide round">

<!-- Top Media Header -->
<div class="mdl-card__media card-header">
<div class="mdl-card__media card-header round-top">
<div class="mdl-card__title white">
<h3 class="mdl-card__title-text">New Notebook</h3>
</div>
Expand Down
32 changes: 12 additions & 20 deletions components/jupyter-web-app/default/kubeflow/jupyter/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
# -*- coding: utf-8 -*-
import yaml
import logging
import sys

CONFIG = "/etc/config/spawner_ui_config.yaml"


def create_logger(name):
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter(
'%(asctime)s | %(name)s | %(levelname)s | %(message)s'))
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
logger.addHandler(handler)
return logger


# Functions for handling the JWT token
def load_file(filepath):
with open(filepath, 'r') as f:
Expand Down Expand Up @@ -61,26 +73,6 @@ def create_notebook_template():
return notebook


def create_pvc_template():
pvc = {
"apiVersion": "v1",
"kind": "PersistentVolumeClaim",
"metadata": {
"name": "",
"namespace": "",
},
"spec": {
"accessModes": [],
"resources": {
"requests": {
"storage": ""
}
},
}
}
return pvc


def set_notebook_names(nb, body):
nb['metadata']['name'] = body["nm"]
nb['metadata']['labels']['app'] = body["nm"]
Expand Down
Loading