diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f124067..71bd9aa 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,8 +1,10 @@ +# Title + Title the PR to complete the sentence: "Merging this PR will ..." ## What -Describe what you have changed and *why* +Describe what you have changed and _why_ ## How to review @@ -11,4 +13,4 @@ Describe what you have changed and *why* 3. Profit! Provide [http://example.com](links) to relevant tickets, articles or other -resources. \ No newline at end of file +resources. diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..e53cb3f --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,31 @@ +--- +name: ๐Ÿ” Dependency Review + +on: + pull_request: + branches: + - main + types: + - edited + - opened + - reopened + - synchronize + +permissions: {} + +jobs: + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + id: checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Dependency Review + id: dependency_review + uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 + with: + fail-on-severity: critical diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..94da09b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,91 @@ +--- +name: ๐Ÿ”– Release + +on: + push: + tags: + - "*" + +permissions: {} + +jobs: + release: + name: Release + runs-on: ubuntu-latest + permissions: + actions: read + attestations: write + contents: write + id-token: write + packages: write + steps: + - name: Checkout + id: checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install cosign + id: install_cosign + uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 + + - name: Log in to GitHub Container Registry + id: ghcr_login + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and Push + id: build_and_push + uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0 + with: + push: true + tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }} + + - name: Sign + id: sign + shell: bash + run: | + cosign sign --yes ghcr.io/${{ github.repository }}@${{ steps.build_and_push.outputs.digest }} + + - name: Generate SBOM + id: generate_sbom + uses: anchore/sbom-action@55dc4ee22412511ee8c3142cbea40418e6cec693 # v0.17.8 + with: + image: ghcr.io/${{ github.repository }}:${{ github.ref_name }} + format: cyclonedx-json + output-file: "sbom.cyclonedx.json" + + - name: Attest + uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 + id: attest + with: + subject-name: ghcr.io/${{ github.repository }} + subject-digest: ${{ steps.build_and_push.outputs.digest }} + push-to-registry: true + + - name: Attest SBOM + uses: actions/attest-sbom@5026d3663739160db546203eeaffa6aa1c51a4d6 # v1.4.1 + id: attest_sbom + with: + subject-name: ghcr.io/${{ github.repository }} + subject-digest: ${{ steps.build_and_push.outputs.digest }} + sbom-path: sbom.cyclonedx.json + push-to-registry: true + + - name: cosign Verify + id: cosign_verify + shell: bash + run: | + cosign verify \ + --certificate-oidc-issuer=https://token.actions.githubusercontent.com \ + --certificate-identity=https://github.com/${{ github.workflow_ref }} \ + ghcr.io/${{ github.repository }}@${{ steps.build_and_push.outputs.digest }} + + - name: GitHub Attestation Verify + id: gh_attestation_verify + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + gh attestation verify oci://ghcr.io/${{ github.repository }}:${{ github.ref_name }} --repo ${{ github.repository }} diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml new file mode 100644 index 0000000..303cd9d --- /dev/null +++ b/.github/workflows/scan.yml @@ -0,0 +1,40 @@ +--- +name: ๐Ÿฉป Scan + +on: + pull_request: + branches: + - main + +permissions: {} + +jobs: + scan: + name: Scan + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + id: checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Build + id: build + shell: bash + env: + IMAGE_NAME: ghcr.io/${{ github.repository }} + IMAGE_TAG: ${{ github.sha }} + run: | + make build + + - name: Scan + id: scan + uses: aquasecurity/trivy-action@18f2510ee396bbf400402947b394f2dd8c87dbb0 # v0.29.0 + env: + TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db:2 + TRIVY_JAVA_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-java-db:1 + with: + image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }} + severity: HIGH,CRITICAL + exit-code: 1 diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml new file mode 100644 index 0000000..dc9b7f9 --- /dev/null +++ b/.github/workflows/super-linter.yml @@ -0,0 +1,35 @@ +--- +name: ๐Ÿฆ Super-Linter + +on: + pull_request: + branches: + - main + types: + - edited + - opened + - reopened + - synchronize + +permissions: {} + +jobs: + super-linter: + name: Super-Linter + runs-on: ubuntu-latest + permissions: + contents: read + statuses: write + steps: + - name: Checkout + id: checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Super-Linter + id: super_linter + uses: super-linter/super-linter/slim@e1cb86b6e8d119f789513668b4b30bf17fe1efe4 # v7.2.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VALIDATE_LUA: false diff --git a/.github/workflows/test-and-push-docker-image.yaml b/.github/workflows/test-and-push-docker-image.yaml deleted file mode 100644 index 70923e2..0000000 --- a/.github/workflows/test-and-push-docker-image.yaml +++ /dev/null @@ -1,101 +0,0 @@ -name: Run tests and push Docker image on success - -on: - push: - branches: [main] - pull_request: - release: - -jobs: - test-and-push: - runs-on: [self-hosted, management-ecr] - env: - APP_HOST: jupyter-lab - APP_PORT: 8888 - PROXY_PORT: 3000 - AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} - AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }} - AUTH0_TENANT_DOMAIN: ${{ secrets.TENANT_DOMAIN }} - LOGOUT_URL: https://cpanel-master.services.dev.mojanalytics.xyz - TEST_TAG: ministryofjustice/nginx-jupyter:test - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Set up Docker Context for Buildx - id: buildx-context - run: docker context use builders || docker context create builders - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v1 - with: - version: latest - endpoint: builders - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-region: eu-west-1 - role-to-assume: arn:aws:iam::593291632749:role/github-actions-management-ecr - role-duration-seconds: 1200 - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 - with: - registries: 593291632749 - - - name: Prep Tags - id: prep - run: | - TAG=noop - if [[ $GITHUB_REF == refs/tags/* ]]; then - TAG=${GITHUB_REF#refs/tags/} - elif [[ $GITHUB_REF == refs/heads/* ]]; then - TAG=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g') - if [ "${{ github.event.repository.default_branch }}" = "$TAG" ]; then - TAG=edge - fi - elif [[ $GITHUB_REF == refs/pull/* ]]; then - TAG=pr-${{ github.event.number }} - elif [ "${{ github.event_name }}" = "push" ]; then - TAG="sha-${GITHUB_SHA::8}" - fi - echo ::set-output name=tag::${TAG} - echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ') - - - name: Clean (docker-compose) - run: make clean - env: - IMAGE_TAG: ${{ steps.prep.outputs.tag }} - - - name: Build image - run: make build - env: - NETWORK: default - REGISTRY: ${{ steps.login-ecr.outputs.registry }} - IMAGE_TAG: ${{ steps.prep.outputs.tag }} - - - name: Spin up (docker-compose) - run: make up - env: - IMAGE_TAG: ${{ steps.prep.outputs.tag }} - - - name: Sleep for 10 seconds - uses: whatnick/wait-action@master - with: - time: '10s' - - - name: Test (docker-compose) - run: make integration - - - name: Push image - run: make push - env: - REGISTRY: ${{ steps.login-ecr.outputs.registry }} - IMAGE_TAG: ${{ steps.prep.outputs.tag }} - - - name: Clean up (docker-compose) - if: always() - run: make clean diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..102c782 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,41 @@ +--- +name: ๐Ÿงช Test + +on: + pull_request: + branches: + - main + +permissions: {} + +jobs: + test: + name: Test + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + id: checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set Up Container Structure Test + id: setup_container_structure_test + uses: ministryofjustice/github-actions/setup-container-structure-test@ccf9e3a4a828df1ec741f6c8e6ed9d0acaef3490 # v18.5.0 + + - name: Test + id: test + shell: bash + env: + IMAGE_TAG: ${{ github.sha }} + # from orig test script + APP_HOST: jupyter-lab + APP_PORT: 8888 + PROXY_PORT: 3000 + AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} + AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }} + AUTH0_TENANT_DOMAIN: ${{ secrets.TENANT_DOMAIN }} + LOGOUT_URL: https://cpanel-master.services.dev.mojanalytics.xyz + TEST_TAG: ministryofjustice/nginx-jupyter:test + run: | + make test diff --git a/Makefile b/Makefile index 18740e5..aa226b9 100644 --- a/Makefile +++ b/Makefile @@ -1,30 +1,30 @@ -export REGISTRY:=593291632749.dkr.ecr.eu-west-1.amazonaws.com +export REGISTRY:= ghcr.io/ministryofjustice/nginx-proxy-jupyter export NETWORK?=default export REPOSITORY:=nginx-proxy-jupyter export VERSION?=0.0.1 -export IMAGE_TAG?=UNSET +export IMAGE_TAG?= local export PROXY_PORT?=8001 export USERNAME?=test-user export REDIRECT_DOMAIN?=127-0-0-1.nip.io:8001 export DOCKER_BUILDKIT=1 clean: - docker-compose down --volumes --remove-orphans + docker compose down --volumes --remove-orphans pull: - docker-compose pull + docker compose pull push: docker push ${REGISTRY}/${REPOSITORY}:${IMAGE_TAG} build: - (docker rmi ${REGISTRY}/${REPOSITORY}:${IMAGE_TAG} || true) && docker build --network=${NETWORK} -t ${REGISTRY}/${REPOSITORY}:${IMAGE_TAG} nginx-proxy + docker build --network=${NETWORK} -t ${REGISTRY}:${IMAGE_TAG} nginx-proxy -up: - docker-compose up -d jupyter-lab nginx-proxy +up: + docker compose up -d jupyter-lab nginx-proxy logs: - docker-compose logs -f nginx-proxy + docker compose logs -f nginx-proxy integration: ./tests/check_is_redirecting.sh diff --git a/README.md b/README.md index 620b549..4d34d79 100644 --- a/README.md +++ b/README.md @@ -9,20 +9,21 @@ Jupyterlab is deployed by the Control Panel. When deployed the pod looks like th The authentication is done using an OIDC call to Auth0, the call is written in lua. N.B. There may in future be a better solution, however at time of implementation, lua was the nginx community recommended solution for this authentication flow. + ## Usage locally All interactions are performed via the `Makefile`. -| Makefile command | Description | -| ---------------- | ----------- | -| clean | stop & destroy containers | -| pull | pull the latest image (unused) | -| push | push the image to a docker repository (gha-only) | -| build | build the proxy image | -| up | start the proxy and jupyter in daemon mode | -| logs | see the logs | -| integration | just run the tests (for gha) | -| test | run the test process including start and cleanup | +| Makefile command | Description | +| ---------------- | ------------------------------------------------ | +| clean | stop & destroy containers | +| pull | pull the latest image (unused) | +| push | push the image to a docker repository (gha-only) | +| build | build the proxy image | +| up | start the proxy and jupyter in daemon mode | +| logs | see the logs | +| integration | just run the tests (for gha) | +| test | run the test process including start and cleanup | ### Running the Application Locally @@ -33,7 +34,7 @@ directory. Run `make build` then `make up` to start the proxy and Jupyter. Once up, you'll find it at http://__-jupyter-lab.127-0-0-1.nip.io:8001/. -On the callback from Auth0, you'll need to remove the s from the https in the callback url, +On the callback from Auth0, you'll need to remove the s from the https in the callback URL, because this doesn't have http support. (https is handled by kubernetes ingress in production.) @@ -46,7 +47,7 @@ To restart and rebuild, `make clean build up`. ## Environment Variables | Environment Variable | Description | -| -------------------- | -----------------------------------------------------| +| -------------------- | ---------------------------------------------------- | | APP_HOST | The hostname of the proxied app (e.g. rstudio) | | APP_PORT | Port on which the proxied app is listening (eg 8787) | | USERNAME | GitHub username of the person whose instance this is | @@ -73,6 +74,7 @@ The nginx-proxy-jupyter container is deployed via helm chart without using the M It will need the above environment variables to be set; the startup script will complain if they are not. Currently it's used in the following charts + - jupyter-lab - jupyter-lab-all-spark - jupyter-lab-datascience-notebook diff --git a/docker-compose.yaml b/docker-compose.yaml index b2a5d93..0daa9fd 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,11 +4,11 @@ version: "3.9" services: jupyter-lab: image: jupyter/pyspark-notebook - ports: [8888:8888,8050:8050] + ports: [8888:8888, 8050:8050] volumes: [./home:/home/test_user] nginx-proxy: - image: ${REGISTRY}/${REPOSITORY}:${IMAGE_TAG} + image: ${REPOSITORY}:${IMAGE_TAG} ports: [$PROXY_PORT:3000] build: network: ${NETWORK:-default} diff --git a/nginx-proxy/Dockerfile b/nginx-proxy/Dockerfile index 7e191b6..974e7e3 100644 --- a/nginx-proxy/Dockerfile +++ b/nginx-proxy/Dockerfile @@ -1,10 +1,16 @@ +#checkov:skip=CKV_DOCKER_3: Migrate as is + FROM openresty/openresty:alpine-fat RUN mkdir /var/log/nginx -RUN apk add --no-cache openssl-dev git gcc +RUN apk add --no-cache \ + "openssl-dev=3.3.2-r1" \ + "git=2.45.2-r0" \ + "gcc=13.2.1_git20240309-r0" RUN luarocks install lua-resty-openidc COPY nginx.conf.template /config/nginx.conf.template -COPY scripts/start_proxy.sh /usr/bin/start_proxy.sh -RUN chmod +x /usr/bin/start_proxy.sh +COPY --chmod=0755 scripts/start_proxy.sh /usr/bin/start_proxy.sh COPY custom_404.html /var/www/html -CMD /usr/bin/start_proxy.sh +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD ["/usr/local/bin/healthcheck.sh"] + +CMD ["/usr/bin/start_proxy.sh"] diff --git a/nginx-proxy/custom_404.html b/nginx-proxy/custom_404.html index 35c8019..3a1636b 100644 --- a/nginx-proxy/custom_404.html +++ b/nginx-proxy/custom_404.html @@ -1,27 +1,25 @@ - + - - - - 404 Error - + + 404 Error - + - +

Error 404: Not found

- The requested URL was not found on this server. Please check the URL and try again. - - - - \ No newline at end of file + The requested URL was not found on this server. Please check the URL and + try again. + + diff --git a/nginx-proxy/scripts/start_proxy.sh b/nginx-proxy/scripts/start_proxy.sh old mode 100644 new mode 100755 index 1d4a371..c30aeb2 --- a/nginx-proxy/scripts/start_proxy.sh +++ b/nginx-proxy/scripts/start_proxy.sh @@ -1,18 +1,17 @@ #!/bin/bash -check_vars() -{ - var_names=("$@") - for var_name in "${var_names[@]}"; do - [ -z "${!var_name}" ] && echo "$var_name is unset." && var_unset=true - done - [ -n "$var_unset" ] && exit 1 - return 0 +check_vars() { + var_names=("$@") + for var_name in "${var_names[@]}"; do + [ -z "${!var_name}" ] && echo "$var_name is unset." && var_unset=true + done + [ -n "$var_unset" ] && exit 1 + return 0 } -# set -u checks if the values are set, but docker-compose is setting the variables to be blank strings if never set, +# set -u checks if the values are set, but docker-compose is setting the variables to be blank strings if never set, # so this checks if they are blank OR unset. check_vars REDIRECT_DOMAIN AUTH0_TENANT_DOMAIN AUTH0_CLIENT_ID AUTH0_CLIENT_SECRET LOGOUT_URL APP_HOST APP_PORT USERNAME -envsubst '${REDIRECT_DOMAIN} ${AUTH0_TENANT_DOMAIN} ${AUTH0_CLIENT_ID} ${AUTH0_CLIENT_SECRET} ${LOGOUT_URL} ${APP_HOST} ${APP_PORT} ${USERNAME}' < /config/nginx.conf.template > /config/nginx.conf +envsubst "${REDIRECT_DOMAIN} ${AUTH0_TENANT_DOMAIN} ${AUTH0_CLIENT_ID} ${AUTH0_CLIENT_SECRET} ${LOGOUT_URL} ${APP_HOST} ${APP_PORT} ${USERNAME}" /config/nginx.conf /usr/local/openresty/nginx/sbin/nginx -g 'daemon off;' -c /config/nginx.conf diff --git a/tests/check_is_redirecting.sh b/tests/check_is_redirecting.sh index 836cca3..c8740ff 100755 --- a/tests/check_is_redirecting.sh +++ b/tests/check_is_redirecting.sh @@ -1,12 +1,12 @@ #!/bin/bash -docker-compose logs +docker compose logs -cmd=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:$PROXY_PORT/) +cmd=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:"$PROXY_PORT"/) if [[ $cmd -ne 302 ]]; then - echo "Test failed because curl responded with a $cmd" - exit 1 + echo "Test failed because curl responded with a $cmd" + exit 1 else - echo "Test passed" - exit 0 + echo "Test passed" + exit 0 fi