diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9cc46a6..b16fb4a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,23 +14,48 @@ jobs: matrix: ep_weave: [false, true] nbsearch: [false, true] + jenkins: [false, true] include: - ep_weave: false nbsearch: false + jenkins: false compose_files: '' label: 'default' - ep_weave: true nbsearch: false + jenkins: false compose_files: '-f docker-compose.yml -f docker-compose.ep_weave.yml' label: 'ep_weave' - nbsearch: true ep_weave: false + jenkins: false compose_files: '-f docker-compose.yml -f docker-compose.nbsearch.yml' label: 'nbsearch' - ep_weave: true nbsearch: true + jenkins: false compose_files: '-f docker-compose.yml -f docker-compose.ep_weave.yml -f docker-compose.nbsearch.yml' - label: 'ep_weave_and_nbsearch' + label: 'ep_weave-and-nbsearch' + - ep_weave: false + nbsearch: false + jenkins: true + compose_files: '-f docker-compose.yml -f docker-compose.jenkins.yml' + label: 'jenkins' + - ep_weave: true + nbsearch: true + jenkins: true + compose_files: '-f docker-compose.yml -f docker-compose.ep_weave.yml -f docker-compose.nbsearch.yml -f docker-compose.jenkins.yml' + label: 'all-services' + exclude: + - ep_weave: false + nbsearch: false + jenkins: true + - ep_weave: true + nbsearch: false + jenkins: true + - ep_weave: false + nbsearch: true + jenkins: true runs-on: ubuntu-22.04 name: Build and Test (${{ matrix.label }}) steps: @@ -152,7 +177,27 @@ jobs: done if ! curl -s -o /dev/null -w "%{http_code}" --insecure https://localhost/services/solr | grep -q "302"; then echo "solr did not start" - sudo docker compose ${{ matrix.compose_files }} logs nbsearch + sudo docker compose ${{ matrix.compose_files }} logs nbsearch-solr + exit 1 + fi + - name: Wait for jenkins to start + if: matrix.jenkins + run: | + set -xe + + echo "Waiting for jenkins to start" + max_retries=10 + while [ $max_retries -gt 0 ]; do + if curl -s -o /dev/null -w "%{http_code}" --insecure https://localhost/services/jenkins | grep -q "301"; then + break + fi + curl -vvvv --insecure https://localhost/services/jenkins + max_retries=$((max_retries-1)) + sleep 10 + done + if ! curl -s -o /dev/null -w "%{http_code}" --insecure https://localhost/services/jenkins | grep -q "301"; then + echo "jenkins did not start" + sudo docker compose ${{ matrix.compose_files }} logs jenkins exit 1 fi - name: Install python for playwright-tests @@ -175,6 +220,7 @@ jobs: export OPHUB_IGNORE_HTTPS_ERRORS=1 export OPHUB_SERVICE_EP_WEAVE=${{ matrix.ep_weave }} export OPHUB_SERVICE_NBSEARCH=${{ matrix.nbsearch }} + export OPHUB_SERVICE_JENKINS=${{ matrix.jenkins }} export OPHUB_USER_IS_ADMIN=1 cd playwright-tests diff --git a/README.md b/README.md index 74ea1bf..1d4b0cd 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,61 @@ You should define the environment variables for ep_weave in the `.env` file to s You can see the ep_weave dashboard from the Services > ep_weave in the Control Panel of the JupyterHub. +### Launching Jenkins + +You can use [jenkins](https://www.jenkins.io/) for CI/CD together with JupyterHub. +If you want to use jenkins, you can start it with JupyterHub by the following command. + + $ sudo docker compose -f docker-compose.yml -f docker-compose.jenkins.yml build + $ sudo docker compose -f docker-compose.yml -f docker-compose.jenkins.yml up -d + +You should define the environment variables for jenkins in the `.env` file to secure the service. + + JENKINS_OAUTH_CLIENT_ID=(random string) + JENKINS_OAUTH_CLIENT_SECRET=(random string) + +You can see the jenkins dashboard from the Services > jenkins in the Control Panel of the JupyterHub. +By default, anonymous users can access jenkins, so you should set the security settings on the Manage Jenkins > Security page. + +![Security Settings of Jenkins](docs/jenkins-security-settings.png) + +You should set the following items. + +- Security Realm - Login with OpenID Connect +- Client id - (random string) of JENKINS_OAUTH_CLIENT_ID +- Client secret - (random string) of JENKINS_OAUTH_CLIENT_SECRET +- Configuration mode - `Discovery via well-known endpoint` + - Well-known configuration endpoint - `http://jupyterhub:8000/services/oidcp-jenkins/internal/.well-known/openid-configuration` + - Advanced > Override scopes - `openid email` +- Advanced configuration > User fields + - User name field name - `sub` + - Username case sensitivity - `Case sensitive` + - Full name field name - `name` + - Email field name - `email` + +The settings below should be change after you have confirmed that the login works. + +- Authorization - Logged-in users can do anything +- Allow anonymous read access - Unchecked + +After you set the security settings, you can log in to jenkins with the account of the JupyterHub. + +You can perform the docker command in the jenkins container to execute notebooks. +The shell script below is an example of executing a notebook using papermill in the jenkins container. + +``` +# execute notebook using papermill +docker run --rm \ + -v /home/john/notebooks/test.ipynb:/home/jovyan/test.ipynb:ro \ + your-single-user-image \ + papermill /home/jovyan/test.ipynb /home/jovyan/output.ipynb +``` + +The `/var/jenkins_home/jobs/` directory in the jenkins container is mapped to the `~/notebooks/share/jenkins-jobs/` directory in the user's container. +All users can read and write(if they have the permission) to the `~/notebooks/share/jenkins-jobs/` directory. + +In addition, only administrators of the JupyterHub can access the Jenkins in JupyterHub. To access Jenkins, users must have the `access:services!service=oidcp-jenkins` permission. + # Create new user For creating a new user, use `useradd` command on the command line of the server. diff --git a/config/jenkins/jupyterhub_config.py b/config/jenkins/jupyterhub_config.py new file mode 100644 index 0000000..9f5e9b0 --- /dev/null +++ b/config/jenkins/jupyterhub_config.py @@ -0,0 +1,43 @@ +from distutils.util import strtobool +import os + +from jupyterhub_oidcp import configure_jupyterhub_oidcp + +# Admin users can access the Jenkins service +# because the `access:services!service=oidcp-jenkins` scope is required +# To access the Jenkins service by non-admin users, +# the `access:services!service=oidcp-jenkins` scope should be added to the user's role + +server_name = os.environ['SERVER_NAME'] + +c.JupyterHub.services.append( + { + 'name': 'jenkins', + 'url': f'http://jenkins-proxy/', + } +) + +oidc_services = [] + +enable_jenkins = os.environ.get('JENKINS_ENABLE_OIDC_SERVICE', None) +if enable_jenkins is not None and bool(strtobool(enable_jenkins)): + oidc_services.append({ + "oauth_client_id": os.environ['JENKINS_OAUTH_CLIENT_ID'], + "api_token": os.environ['JENKINS_OAUTH_CLIENT_SECRET'], + "redirect_uris": [f"https://{server_name}/services/jenkins/securityRealm/finishLogin"], + }) + +if len(oidc_services) > 0: + configure_jupyterhub_oidcp( + c, + port=8889, + service_name="oidcp-jenkins", + issuer="http://jupyterhub:8000/services/oidcp-jenkins/internal/", + base_url=f"https://{server_name}/", + internal_base_url="http://jupyterhub:8000", + debug=True, + services=oidc_services, + vault_path="./tmp/jupyterhub_oid/.jenkins.vault", + admin_email_pattern="{uid}@admin.jupyterhub", + user_email_pattern="{uid}@user.jupyterhub", + ) diff --git a/config/nbsearch/jupyterhub_config.py b/config/nbsearch/jupyterhub_config.py index 94a4918..8eee028 100644 --- a/config/nbsearch/jupyterhub_config.py +++ b/config/nbsearch/jupyterhub_config.py @@ -3,7 +3,6 @@ c.JupyterHub.services.append( { 'name': service_name, - 'admin': True, 'url': f'http://nbsearch-{service_name}-proxy', } ) diff --git a/config/oidc/jupyterhub_config.py b/config/oidc/jupyterhub_config.py index 087f07e..39e1148 100644 --- a/config/oidc/jupyterhub_config.py +++ b/config/oidc/jupyterhub_config.py @@ -3,10 +3,11 @@ from jupyterhub_oidcp import configure_jupyterhub_oidcp +# All users can access the OpenID Connect service c.JupyterHub.load_roles = [ { 'name': 'user', - 'scopes': ['self', 'access:services'], + 'scopes': ['self', 'access:services!service=oidcp'], } ] @@ -32,12 +33,12 @@ if len(oidc_services) > 0: configure_jupyterhub_oidcp( c, + service_name="oidcp", issuer="http://jupyterhub:8000/services/oidcp/internal/", base_url=f"https://{server_name}/", internal_base_url="http://jupyterhub:8000", debug=True, services=oidc_services, - oauth_client_allowed_scopes=None, vault_path="./tmp/jupyterhub_oid/.vault", admin_email_pattern="{uid}@admin.jupyterhub", user_email_pattern="{uid}@user.jupyterhub", diff --git a/docker-compose.jenkins.yml b/docker-compose.jenkins.yml new file mode 100644 index 0000000..7a88f32 --- /dev/null +++ b/docker-compose.jenkins.yml @@ -0,0 +1,34 @@ +services: + jenkins: + build: ./images/jenkins + environment: + - JENKINS_OPTS="--prefix=/services/jenkins" + - JAVA_OPTS="-Djenkins.install.runSetupWizard=false" + - USERS_DIR="/home/user-notebooks" + networks: + - backend + restart: on-failure + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/lib/jenkins:/var/jenkins_home + - /var/lib/jupyterhub/share/jenkins-jobs:/var/jenkins_home/jobs + - /var/run/docker.sock:/var/run/docker.sock + - ${USERS_DIR:-/home/user-notebooks}:/home/user-notebooks:ro + + jenkins-proxy: + build: + context: ./images/jenkins-proxy + depends_on: + - jenkins + restart: always + networks: + - backend + + jupyterhub: + environment: + JENKINS_ENABLE_OIDC_SERVICE: "1" + JENKINS_OAUTH_CLIENT_ID: ${JENKINS_OAUTH_CLIENT_ID:-jenkins_oauth_client_changeme} + JENKINS_OAUTH_CLIENT_SECRET: ${JENKINS_OAUTH_CLIENT_SECRET:-jenkins_oauth_secret_changeme} + SERVER_NAME: "${SERVER_NAME}" + volumes: + - './config/jenkins/jupyterhub_config.py:/jupyterhub_config.d/jenkins.py:ro' diff --git a/docs/jenkins-security-settings.png b/docs/jenkins-security-settings.png new file mode 100644 index 0000000..8341c4a Binary files /dev/null and b/docs/jenkins-security-settings.png differ diff --git a/images/jenkins-proxy/Dockerfile b/images/jenkins-proxy/Dockerfile new file mode 100644 index 0000000..62d8ef6 --- /dev/null +++ b/images/jenkins-proxy/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx:stable + +COPY ./nginx.conf /etc/nginx/nginx.conf diff --git a/images/jenkins-proxy/nginx.conf b/images/jenkins-proxy/nginx.conf new file mode 100644 index 0000000..d4ec4ac --- /dev/null +++ b/images/jenkins-proxy/nginx.conf @@ -0,0 +1,34 @@ +events { + worker_connections 4096; ## Default: 1024 +} + +http { + + client_max_body_size 50M; + + server { + listen 80; + + location /services/jenkins { + proxy_pass http://jenkins:8080/services/jenkins; + proxy_redirect off; + proxy_buffering off; + proxy_set_header Host $host; + proxy_pass_header Server; + + # Enabling WebSocket + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + + # Adjust timeout for WebSocket + proxy_read_timeout 60s; + proxy_send_timeout 60s; + + proxy_set_header X-Real-IP $remote_addr; # https://nginx.org/en/docs/http/ngx_http_proxy_module.html + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Port "443"; + } + } +} diff --git a/images/jenkins/Dockerfile b/images/jenkins/Dockerfile new file mode 100644 index 0000000..8046f13 --- /dev/null +++ b/images/jenkins/Dockerfile @@ -0,0 +1,12 @@ +FROM jenkins/jenkins:lts + +# Jenkins is running as root to manipulate docker +USER root + +RUN curl -fsSL https://get.docker.com | sh +RUN apt-get update && \ + apt-get install -y zip && \ + apt-get autoclean && apt-get clean && apt-get autoremove + +RUN jenkins-plugin-cli --plugins \ + "oic-auth" "build-timeout" "discard-old-build" slack diff --git a/playwright-tests/tests/conftest.py b/playwright-tests/tests/conftest.py index e910fc9..02ec58b 100644 --- a/playwright-tests/tests/conftest.py +++ b/playwright-tests/tests/conftest.py @@ -47,6 +47,10 @@ def ophub_has_ep_weave() -> bool: def ophub_has_nbsearch() -> bool: return os.environ.get('OPHUB_SERVICE_NBSEARCH', '') in ('1', 'yes', 'true') +@pytest.fixture +def ophub_has_jenkins() -> bool: + return os.environ.get('OPHUB_SERVICE_JENKINS', '') in ('1', 'yes', 'true') + @pytest.fixture def ophub_login_to_hub_home(ophub_url, ophub_username, ophub_password) -> Callable[[Page], None]: def login(page: Page): @@ -63,10 +67,12 @@ def login(page: Page): return login @pytest.fixture -def ophub_services(ophub_has_ep_weave, ophub_has_nbsearch) -> List[str]: +def ophub_services(ophub_has_ep_weave, ophub_has_nbsearch, ophub_has_jenkins) -> List[str]: services = [] if ophub_has_ep_weave: services.append('ep_weave') if ophub_has_nbsearch: services.append('solr') + if ophub_has_jenkins: + services.append('jenkins') return services