diff --git a/.github/workflows/cd-pipeline.yml b/.github/workflows/cd-pipeline.yml index 5e18367970..7b3704c9c5 100644 --- a/.github/workflows/cd-pipeline.yml +++ b/.github/workflows/cd-pipeline.yml @@ -35,6 +35,7 @@ on: # rebuild any PRs and main branch changes - 'bitnami/jasperreports/**' - 'bitnami/jenkins/**' - 'bitnami/joomla/**' + - 'bitnami/jupyterhub/**' - 'bitnami/kafka/**' - 'bitnami/keycloak/**' - 'bitnami/kibana/**' diff --git a/.vib/jupyterhub/cypress/cypress.json b/.vib/jupyterhub/cypress/cypress.json new file mode 100644 index 0000000000..fa21f6ee38 --- /dev/null +++ b/.vib/jupyterhub/cypress/cypress.json @@ -0,0 +1,8 @@ +{ + "baseUrl": "http://localhost/", + "defaultCommandTimeout": 30000, + "env": { + "username": "test_user", + "password": "ComplicatedPassword123!4" + } +} diff --git a/.vib/jupyterhub/cypress/cypress/fixtures/notebook_template.ipynb b/.vib/jupyterhub/cypress/cypress/fixtures/notebook_template.ipynb new file mode 100644 index 0000000000..fdf479ce0a --- /dev/null +++ b/.vib/jupyterhub/cypress/cypress/fixtures/notebook_template.ipynb @@ -0,0 +1,51 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "908534a5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hello World!" + ] + } + ], + "source": [ + "print(\"Hello\" + \" World!\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "215c7ef4", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.vib/jupyterhub/cypress/cypress/integration/jupyterhub_spec.js b/.vib/jupyterhub/cypress/cypress/integration/jupyterhub_spec.js new file mode 100644 index 0000000000..2254606be5 --- /dev/null +++ b/.vib/jupyterhub/cypress/cypress/integration/jupyterhub_spec.js @@ -0,0 +1,44 @@ +/// +import { random } from '../support/utils'; + +it('allows to upload and execute a python notebook', () => { + const notebookName = `notebook_template_${random}.ipynb`; + const userName = Cypress.env('username'); + + cy.login(); + cy.visit(`/user/${userName}/tree/tmp`); + cy.contains('Upload').should('be.visible'); + cy.get('[type=file]').selectFile('cypress/fixtures/notebook_template.ipynb', { + force: true, + }); + cy.get('.filename_input').clear().type(notebookName); + cy.contains('button', 'Upload').click(); + cy.contains('a', notebookName); + + cy.visit(`/user/${userName}/notebooks/tmp/${notebookName}`); + cy.contains('button', 'Run').click(); + cy.contains('Hello World!'); +}); + +it('allows generating an API token', () => { + cy.login(); + cy.visit('/hub/token'); + // We need to wait until the background API request is finished + cy.contains(/\d+Z/).should('not.exist'); + cy.contains('button', 'API token').click(); + cy.get('#token-result') + .should('be.visible') + .invoke('text') + .then((apiToken) => { + cy.request({ + url: '/hub/api/users', + method: 'GET', + headers: { + Authorization: `token ${apiToken}`, + }, + }).then((response) => { + expect(response.status).to.eq(200); + expect(response.body[0].name).to.eq(Cypress.env('username')); + }); + }); +}); diff --git a/.vib/jupyterhub/cypress/cypress/support/commands.js b/.vib/jupyterhub/cypress/cypress/support/commands.js new file mode 100644 index 0000000000..34e1a0676f --- /dev/null +++ b/.vib/jupyterhub/cypress/cypress/support/commands.js @@ -0,0 +1,35 @@ +const COMMAND_DELAY = 2000; + +for (const command of ['click']) { + Cypress.Commands.overwrite(command, (originalFn, ...args) => { + const origVal = originalFn(...args); + + return new Promise((resolve) => { + setTimeout(() => { + resolve(origVal); + }, COMMAND_DELAY); + }); + }); +} + +Cypress.Commands.add( + 'login', + (username = Cypress.env('username'), password = Cypress.env('password')) => { + cy.visit('/hub/login'); + cy.get('#username_input').type(username); + cy.get('#password_input').type(password); + cy.get('#login_submit').click(); + // The authentication is not completed until the page is rendered + cy.contains('Launcher'); + } +); + +Cypress.on('uncaught:exception', (err, runnable) => { + // we expect a 3rd party library error with message 'list not defined' + // and don't want to fail the test so we return false + if (err.message.includes('Cannot read properties of null')) { + return false; + } + // we still want to ensure there are no other unexpected + // errors, so we let them fail the test +}); diff --git a/.vib/jupyterhub/cypress/cypress/support/index.js b/.vib/jupyterhub/cypress/cypress/support/index.js new file mode 100644 index 0000000000..37a498fb5b --- /dev/null +++ b/.vib/jupyterhub/cypress/cypress/support/index.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/.vib/jupyterhub/cypress/cypress/support/utils.js b/.vib/jupyterhub/cypress/cypress/support/utils.js new file mode 100644 index 0000000000..f0217c9773 --- /dev/null +++ b/.vib/jupyterhub/cypress/cypress/support/utils.js @@ -0,0 +1,3 @@ +/// + +export let random = (Math.random() + 1).toString(36).substring(7); diff --git a/.vib/jupyterhub/goss/goss.yaml b/.vib/jupyterhub/goss/goss.yaml new file mode 100644 index 0000000000..1bd335ab6b --- /dev/null +++ b/.vib/jupyterhub/goss/goss.yaml @@ -0,0 +1,39 @@ +http: + http://jupyterhub-hub:{{ .Vars.hub.service.ports.http }}/hub/health: + status: 200 + http://jupyterhub-proxy-api:{{ .Vars.proxy.service.api.ports.http }}: + status: 404 +file: + /etc/jupyterhub: + exists: true + filetype: directory + mode: "0755" + owner: root + /etc/jupyterhub/jupyterhub_config.py: + exists: true + filetype: file + mode: "0644" + owner: root + contains: + - /hub_container_port.*{{ .Vars.hub.containerPorts.http }}/ + /usr/local/etc/jupyterhub/secret/values.yaml: + exists: true + filetype: symlink + mode: "0777" + owner: root + contains: + - postgresql://{{ .Vars.postgresql.auth.username }}@jupyterhub-postgresql:{{ .Vars.postgresql.service.ports.postgresql }}/{{ .Vars.postgresql.auth.database }} + - /uid.*{{ .Vars.singleuser.containerSecurityContext.runAsUser }}/ + - /fsGid.*{{ .Vars.singleuser.podSecurityContext.fsGroup }}/ + - /type.*dynamic/ + /var/run/secrets/kubernetes.io/serviceaccount: + exists: {{ .Vars.hub.serviceAccount.automountServiceAccountToken }} + filetype: directory + mode: "3777" +command: + check-user-info: + exec: id + exit-status: 0 + stdout: + - uid={{ .Vars.hub.containerSecurityContext.runAsUser }} + - /groups=.*{{ .Vars.hub.podSecurityContext.fsGroup }}/ diff --git a/.vib/jupyterhub/goss/vars.yaml b/.vib/jupyterhub/goss/vars.yaml new file mode 100644 index 0000000000..dc6768e01d --- /dev/null +++ b/.vib/jupyterhub/goss/vars.yaml @@ -0,0 +1,29 @@ +hub: + containerPorts: + http: 8082 + containerSecurityContext: + runAsUser: 1002 + podSecurityContext: + fsGroup: 1002 + serviceAccount: + automountServiceAccountToken: true + service: + ports: + http: 8082 +proxy: + service: + api: + ports: + http: 8000 +singleuser: + containerSecurityContext: + runAsUser: 1002 + podSecurityContext: + fsGroup: 1002 +postgresql: + auth: + username: bn_vib_jupyterhub + database: bitnami_vib_jupyterhub + service: + ports: + postgresql: 5432 diff --git a/.vib/jupyterhub/vib-publish.json b/.vib/jupyterhub/vib-publish.json index 6a636e6004..04bb25568f 100644 --- a/.vib/jupyterhub/vib-publish.json +++ b/.vib/jupyterhub/vib-publish.json @@ -16,6 +16,56 @@ } ] }, + "verify": { + "context": { + "resources": { + "url": "{SHA_ARCHIVE}", + "path": "/bitnami/jupyterhub" + }, + "runtime_parameters": "aHViOgogIGJhc2VVcmw6IC8KICBhZG1pblVzZXI6IHRlc3RfdXNlcgogIHBhc3N3b3JkOiBDb21wbGljYXRlZFBhc3N3b3JkMTIzITQKICBjb250YWluZXJQb3J0czoKICAgIGh0dHA6IDgwODIKICBjb250YWluZXJTZWN1cml0eUNvbnRleHQ6CiAgICBlbmFibGVkOiB0cnVlCiAgICBydW5Bc1VzZXI6IDEwMDIKICBwb2RTZWN1cml0eUNvbnRleHQ6CiAgICBlbmFibGVkOiB0cnVlCiAgICBmc0dyb3VwOiAxMDAyCiAgc2VydmljZUFjY291bnQ6CiAgICBjcmVhdGU6IHRydWUKICAgIGF1dG9tb3VudFNlcnZpY2VBY2NvdW50VG9rZW46IHRydWUKICByYmFjOgogICAgY3JlYXRlOiB0cnVlCiAgc2VydmljZToKICAgIHBvcnRzOgogICAgICBodHRwOiA4MDgyCnByb3h5OgogIHNlcnZpY2U6CiAgICBhcGk6CiAgICAgIHBvcnRzOgogICAgICAgIGh0dHA6IDgwMDAKICAgIHB1YmxpYzoKICAgICAgdHlwZTogTG9hZEJhbGFuY2VyCiAgICAgIHBvcnRzOgogICAgICAgIGh0dHA6IDgwCmltYWdlUHVsbGVyOgogIGVuYWJsZWQ6IHRydWUKc2luZ2xldXNlcjoKICBjb250YWluZXJTZWN1cml0eUNvbnRleHQ6CiAgICBlbmFibGVkOiB0cnVlCiAgICBydW5Bc1VzZXI6IDEwMDIKICBwb2RTZWN1cml0eUNvbnRleHQ6CiAgICBlbmFibGVkOiB0cnVlCiAgICBmc0dyb3VwOiAxMDAyCiAgcGVyc2lzdGVuY2U6CiAgICBlbmFibGVkOiB0cnVlCnBvc3RncmVzcWw6CiAgZW5hYmxlZDogdHJ1ZQogIGF1dGg6CiAgICB1c2VybmFtZTogYm5fdmliX2p1cHl0ZXJodWIKICAgIGRhdGFiYXNlOiBiaXRuYW1pX3ZpYl9qdXB5dGVyaHViCiAgc2VydmljZToKICAgIHBvcnRzOgogICAgICBwb3N0Z3Jlc3FsOiA1NDMy", + "target_platform": { + "target_platform_id": "{VIB_ENV_TARGET_PLATFORM}", + "size": { + "name": "S4" + } + } + }, + "actions": [ + { + "action_id": "health-check", + "params": { + "endpoint": "lb-jupyterhub-proxy-public-http", + "app_protocol": "HTTP" + } + }, + { + "action_id": "goss", + "params": { + "resources": { + "path": "/.vib/jupyterhub/goss" + }, + "remote": { + "workload": "deploy-jupyterhub-hub" + }, + "vars_file": "vars.yaml" + } + }, + { + "action_id": "cypress", + "params": { + "resources": { + "path": "/.vib/jupyterhub/cypress" + }, + "endpoint": "lb-jupyterhub-proxy-public-http", + "app_protocol": "HTTP", + "env": { + "username": "test_user", + "password": "ComplicatedPassword123!4" + } + } + } + ] + }, "publish": { "actions": [ { diff --git a/.vib/jupyterhub/vib-verify.json b/.vib/jupyterhub/vib-verify.json index 1a09709127..e5f13b1ae6 100644 --- a/.vib/jupyterhub/vib-verify.json +++ b/.vib/jupyterhub/vib-verify.json @@ -15,6 +15,56 @@ "action_id": "helm-lint" } ] + }, + "verify": { + "context": { + "resources": { + "url": "{SHA_ARCHIVE}", + "path": "/bitnami/jupyterhub" + }, + "runtime_parameters": "aHViOgogIGJhc2VVcmw6IC8KICBhZG1pblVzZXI6IHRlc3RfdXNlcgogIHBhc3N3b3JkOiBDb21wbGljYXRlZFBhc3N3b3JkMTIzITQKICBjb250YWluZXJQb3J0czoKICAgIGh0dHA6IDgwODIKICBjb250YWluZXJTZWN1cml0eUNvbnRleHQ6CiAgICBlbmFibGVkOiB0cnVlCiAgICBydW5Bc1VzZXI6IDEwMDIKICBwb2RTZWN1cml0eUNvbnRleHQ6CiAgICBlbmFibGVkOiB0cnVlCiAgICBmc0dyb3VwOiAxMDAyCiAgc2VydmljZUFjY291bnQ6CiAgICBjcmVhdGU6IHRydWUKICAgIGF1dG9tb3VudFNlcnZpY2VBY2NvdW50VG9rZW46IHRydWUKICByYmFjOgogICAgY3JlYXRlOiB0cnVlCiAgc2VydmljZToKICAgIHBvcnRzOgogICAgICBodHRwOiA4MDgyCnByb3h5OgogIHNlcnZpY2U6CiAgICBhcGk6CiAgICAgIHBvcnRzOgogICAgICAgIGh0dHA6IDgwMDAKICAgIHB1YmxpYzoKICAgICAgdHlwZTogTG9hZEJhbGFuY2VyCiAgICAgIHBvcnRzOgogICAgICAgIGh0dHA6IDgwCmltYWdlUHVsbGVyOgogIGVuYWJsZWQ6IHRydWUKc2luZ2xldXNlcjoKICBjb250YWluZXJTZWN1cml0eUNvbnRleHQ6CiAgICBlbmFibGVkOiB0cnVlCiAgICBydW5Bc1VzZXI6IDEwMDIKICBwb2RTZWN1cml0eUNvbnRleHQ6CiAgICBlbmFibGVkOiB0cnVlCiAgICBmc0dyb3VwOiAxMDAyCiAgcGVyc2lzdGVuY2U6CiAgICBlbmFibGVkOiB0cnVlCnBvc3RncmVzcWw6CiAgZW5hYmxlZDogdHJ1ZQogIGF1dGg6CiAgICB1c2VybmFtZTogYm5fdmliX2p1cHl0ZXJodWIKICAgIGRhdGFiYXNlOiBiaXRuYW1pX3ZpYl9qdXB5dGVyaHViCiAgc2VydmljZToKICAgIHBvcnRzOgogICAgICBwb3N0Z3Jlc3FsOiA1NDMy", + "target_platform": { + "target_platform_id": "{VIB_ENV_TARGET_PLATFORM}", + "size": { + "name": "S4" + } + } + }, + "actions": [ + { + "action_id": "health-check", + "params": { + "endpoint": "lb-jupyterhub-proxy-public-http", + "app_protocol": "HTTP" + } + }, + { + "action_id": "goss", + "params": { + "resources": { + "path": "/.vib/jupyterhub/goss" + }, + "remote": { + "workload": "deploy-jupyterhub-hub" + }, + "vars_file": "vars.yaml" + } + }, + { + "action_id": "cypress", + "params": { + "resources": { + "path": "/.vib/jupyterhub/cypress" + }, + "endpoint": "lb-jupyterhub-proxy-public-http", + "app_protocol": "HTTP", + "env": { + "username": "test_user", + "password": "ComplicatedPassword123!4" + } + } + } + ] } } }