Skip to content

Commit

Permalink
move image build checks out of the spawner
Browse files Browse the repository at this point in the history
closes #55
  • Loading branch information
rokroskar committed Sep 21, 2018
1 parent 22a3df0 commit 1ec0d57
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 70 deletions.
2 changes: 1 addition & 1 deletion helm-chart/renku-notebooks/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ apiVersion: v1
appVersion: '1.0'
description: A Helm chart for the Renku Notebooks service
name: renku-notebooks
version: 0.1.0
version: 0.2.0
6 changes: 6 additions & 0 deletions helm-chart/renku-notebooks/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ spec:
value: {{ .Values.jupyterhub.hub.services.notebooks.oauth_client_id }}
- name: GITLAB_REGISTRY_SECRET
value: {{ template "renku.fullname" . }}-registry
- name: GITLAB_URL
{{ if .Values.gitlab_url }}
value: {{ .Values.gitlab_url }}
{{ else }}
value: {{ template "notebooks.http" . }}://{{ .Values.global.renku.domain}}/gitlab
{{ end }}
ports:
- name: http
containerPort: 8000
Expand Down
6 changes: 4 additions & 2 deletions helm-chart/renku-notebooks/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
global:
useHTTPS: false

## specify the GitLab instance URL
# gitlab:
# url:

replicaCount: 1

image:
Expand Down Expand Up @@ -89,8 +93,6 @@ jupyterhub:
extraEnv:
DEBUG: 1
JUPYTERHUB_SPAWNER_CLASS: spawners.RenkuKubeSpawner
## timeout for waiting on an image build
# GITLAB_IMAGE_BUILD_TIMEOUT: 600
## GitLab instance URL
# GITLAB_URL: http://gitlab.com
extraConfig: |
Expand Down
67 changes: 2 additions & 65 deletions jupyterhub/spawners.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,16 @@ def start(self, *args, **kwargs):
auth_state = yield self.user.get_auth_state()
assert 'access_token' in auth_state

# 1. check authorization against GitLab
options = self.user_options
namespace = options.get('namespace')
project = options.get('project')
commit_sha = options.get('commit_sha')
commit_sha_7 = commit_sha[:7]
self.image = options.get('image')

url = os.getenv('GITLAB_URL', 'http://gitlab.renku.build')

# image build timeout -- configurable, defaults to 10 minutes
image_build_timeout = int(os.getenv('GITLAB_IMAGE_BUILD_TIMEOUT', 600))

# check authorization against GitLab
gl = gitlab.Gitlab(
url, api_version=4, oauth_token=auth_state['access_token']
)
Expand All @@ -109,7 +107,6 @@ def start(self, *args, **kwargs):
gl.auth()
gl_project = gl.projects.get('{0}/{1}'.format(namespace, project))
self.gl_user = gl.user
self.log.info('Got user profile: {}'.format(self.gl_user))

# gather project permissions for the logged in user
permissions = gl_project.attributes['permissions']
Expand All @@ -135,57 +132,7 @@ def start(self, *args, **kwargs):
raise web.HTTPError(401, 'Not authorized to view project.')
return

# set default image
self.image = os.getenv(
'JUPYTERHUB_NOTEBOOK_IMAGE', 'renku/singleuser:latest'
)

for pipeline in gl_project.pipelines.list():
if pipeline.attributes['sha'] == commit_sha:
status = self._get_job_status(pipeline, 'image_build')

if not status:
# there is no image_build job for this commit
# so we use the default image
self.log.info('No image_build job found in pipeline.')
break

# we have an image_build job in the pipeline, check status
timein = time.time()
# TODO: remove this loop altogether and only request the launch
# when the image is ready
while time.time() - timein < image_build_timeout:
if status == 'success':
# the image was built
# it *should* be there so lets use it
self.image = '{image_registry}'\
'/{namespace}'\
'/{project}'\
':{commit_sha_7}'.format(
image_registry=os.getenv('IMAGE_REGISTRY'),
commit_sha_7=commit_sha_7,
**options
).lower()
self.log.info(
'Using image {image}.'.format(image=self.image)
)
break
elif status in {'failed', 'canceled'}:
self.log.info(
'Image build failed for project {0} commit {1} - '
'using {2} instead'.format(
project, commit_sha, self.image
)
)
break
yield gen.sleep(5)
status = self._get_job_status(pipeline, 'image_build')
self.log.debug(
'status of image_build job for commit '
'{commit_sha_7}: {status}'.format(
commit_sha_7=commit_sha_7, status=status
)
)

self.cmd = 'jupyterhub-singleuser'
try:
Expand All @@ -203,16 +150,6 @@ def start(self, *args, **kwargs):

return result

@staticmethod
def _get_job_status(pipeline, job_name):
"""Helper method to retrieve job status based on the job name."""
status = [
job.attributes['status'] for job in pipeline.jobs.list()
if job.attributes['name'] == job_name
]
return status.pop() if status else None


try:
import docker
from dockerspawner import DockerSpawner
Expand Down
118 changes: 116 additions & 2 deletions src/notebooks_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
ANNOTATION_PREFIX = 'hub.jupyter.org'
"""The prefix used for annotations by the KubeSpawner."""

GITLAB_URL = os.environ.get('GITLAB_URL', 'https://gitlab.com')
"""The GitLab instance to use."""

SERVER_STATUS_MAP = {'spawn': 'spawning', 'stop': 'stopping'}

# check if we are running on k8s
Expand Down Expand Up @@ -176,6 +179,12 @@ def ui(path):
@authenticated
def whoami(user):
"""Return information about the authenticated user."""
info = get_user_info(user)
return jsonify(info)


def get_user_info(user):
"""Return the full user object."""
headers = {auth.auth_header_name: 'token {0}'.format(auth.api_token)}
info = json.loads(
requests.request(
Expand All @@ -186,7 +195,103 @@ def whoami(user):
headers=headers
).text
)
return jsonify(info)

return info


def get_gitlab_project(user, namespace, project):
"""Retrieve the GitLab project."""
info = get_user_info(user)
auth_state = info.get('auth_state')
assert auth_state

gl = gitlab.Gitlab(
GITLAB_URL, api_version=4, oauth_token=auth_state['access_token']
)

try:
gl.auth()
gl_project = gl.projects.get('{0}/{1}'.format(namespace, project))
gl_user = gl.user
app.logger.info('Got user profile: {}'.format(gl_user))

# gather project permissions for the logged in user
permissions = gl_project.attributes['permissions']
access_level = max([
x[1].get('access_level', 0) for x in permissions.items() if x[1]
])
app.logger.debug(
'access level for user {username} in '
'{namespace}/{project} = {access_level}'.format(
username=user.get('name'),
namespace=namespace,
project=project,
access_level=access_level
)
)
except Exception as e:
app.logger.error(e)
return app.response_class(
status=500, response='There was a problem accessing the project.'
)

if access_level < gitlab.DEVELOPER_ACCESS:
return app.response_class(
status=401, response='Not authorized to view project.'
)

return gl_project


def get_job_status(pipeline, job_name):
"""Helper method to retrieve job status based on the job name."""
status = [
job.attributes['status'] for job in pipeline.jobs.list()
if job.attributes['name'] == job_name
]
return status.pop() if status else None


def get_notebook_image(user, namespace, project, commit_sha):
"""Check if the image for the namespace/project/commit_sha is ready."""
gl_project = get_gitlab_project(user, namespace, project)

# image build timeout -- configurable, defaults to 10 minutes
image_build_timeout = int(os.getenv('GITLAB_IMAGE_BUILD_TIMEOUT', 600))

image = 'renku/singleuser:latest'
commit_sha_7 = commit_sha[:7]

for pipeline in gl_project.pipelines.list():
if pipeline.attributes['sha'] == commit_sha:
status = get_job_status(pipeline, 'image_build')

if not status:
# there is no image_build job for this commit
# so we use the default image
app.logger.info('No image_build job found in pipeline.')

# we have an image_build job in the pipeline, check status
elif status == 'success':
# the image was built
# it *should* be there so lets use it
image = '{image_registry}/{namespace}'\
'/{project}:{commit_sha_7}'.format(
image_registry=os.getenv('IMAGE_REGISTRY'),
commit_sha_7=commit_sha_7,
namespace=namespace,
project=project
).lower()
app.logger.info(f'Using image {image}.')

else:
app.logger.info(
'No image found for project {0} commit {1} - '
'using {2} instead'.format(project, commit_sha, image)
)
break

return image


def get_user_server(user, server_name):
Expand Down Expand Up @@ -272,12 +377,18 @@ def launch_notebook(user, namespace, project, commit_sha, notebook=None):

# 1. launch using spawner that checks the access
headers = {auth.auth_header_name: 'token {0}'.format(auth.api_token)}

image = request.args.get(
'image', get_notebook_image(user, namespace, project, commit_sha)
)

payload = {
'branch': request.args.get('branch', 'master'),
'commit_sha': commit_sha,
'namespace': namespace,
'notebook': notebook,
'project': project,
'image': image,
}
if os.environ.get('GITLAB_REGISTRY_SECRET'):
payload['image_pull_secrets'] = payload.get('image_pull_secrets', [])
Expand All @@ -288,7 +399,10 @@ def launch_notebook(user, namespace, project, commit_sha, notebook=None):
r = requests.request(
'POST',
'{prefix}/users/{user[name]}/servers/{server_name}'.format(
prefix=auth.api_url, user=user, server_name=server_name
prefix=auth.api_url,
user=user,
server_name=server_name,
image=image
),
json=payload,
headers=headers,
Expand Down

0 comments on commit 1ec0d57

Please sign in to comment.