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"
+ }
+ }
+ }
+ ]
}
}
}