From f0a7869ea464f9830aefa230142110832b41f4f0 Mon Sep 17 00:00:00 2001 From: Alex Hill Date: Thu, 18 May 2023 19:43:28 +0100 Subject: [PATCH 1/9] dockerise web app and add to build --- .github/workflows/backend-test-and-build.yml | 3 +- ...d-test.yml => frontend-test-and-build.yml} | 10 ++++ api/scripts/smoke-test | 37 ++++++++++++++ app/Dockerfile | 14 +++++ app/README.md | 11 +++- app/nginx.conf | 51 +++++++++++++++++++ app/scripts/build | 11 ++++ app/scripts/build-and-push | 12 +++++ app/scripts/common | 19 +++++++ app/scripts/run | 10 ++++ app/scripts/smoke-test | 37 ++++++++++++++ scripts/clear-docker | 7 +++ 12 files changed, 220 insertions(+), 2 deletions(-) rename .github/workflows/{frontend-test.yml => frontend-test-and-build.yml} (79%) create mode 100755 api/scripts/smoke-test create mode 100644 app/Dockerfile create mode 100644 app/nginx.conf create mode 100755 app/scripts/build create mode 100755 app/scripts/build-and-push create mode 100755 app/scripts/common create mode 100755 app/scripts/run create mode 100755 app/scripts/smoke-test create mode 100755 scripts/clear-docker diff --git a/.github/workflows/backend-test-and-build.yml b/.github/workflows/backend-test-and-build.yml index fb73d2d0..39e70ad3 100644 --- a/.github/workflows/backend-test-and-build.yml +++ b/.github/workflows/backend-test-and-build.yml @@ -51,4 +51,5 @@ jobs: working-directory: api run: ./scripts/build-and-push - name: Smoke test - run: ./scripts/smoke-test.sh + working-directory: api + run: ./scripts/smoke-test diff --git a/.github/workflows/frontend-test.yml b/.github/workflows/frontend-test-and-build.yml similarity index 79% rename from .github/workflows/frontend-test.yml rename to .github/workflows/frontend-test-and-build.yml index ae95d032..86c7bdc6 100644 --- a/.github/workflows/frontend-test.yml +++ b/.github/workflows/frontend-test-and-build.yml @@ -11,6 +11,10 @@ on: pull_request: branches: - 'main' + +env: + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + jobs: frontend: runs-on: ubuntu-latest @@ -36,3 +40,9 @@ jobs: uses: codecov/codecov-action@v3 with: fail_ci_if_error: true + - name: Build image + working-directory: app + run: ./scripts/build-and-push + - name: Smoke test + working-directory: app + run: ./scripts/smoke-test.sh diff --git a/api/scripts/smoke-test b/api/scripts/smoke-test new file mode 100755 index 00000000..e5c0dcdc --- /dev/null +++ b/api/scripts/smoke-test @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +wait_for() +{ + echo "waiting up to $TIMEOUT seconds for API" + start_ts=$(date +%s) + for i in $(seq $TIMEOUT); do + + result=$(curl --write-out %{http_code} --silent --output /dev/null http://localhost:8080/packets) + if [[ $result -eq 200 ]]; then + end_ts=$(date +%s) + echo "API available after $((end_ts - start_ts)) seconds" + break + fi + sleep 1 + echo "...still waiting" + done + return $result +} + +if [[ -v "BRANCH_NAME" ]]; then + TAG=$BRANCH_NAME +else + TAG=$(git symbolic-ref --short HEAD) +fi + +docker run --network=host -d mrcide/packit-api:$TAG + +# The variable expansion below is 60s by default, or the argument provided +# to this script +TIMEOUT="${1:-60}" +wait_for +RESULT=$? +if [[ $RESULT -ne 200 ]]; then + echo "API did not become available in time" + exit 1 +fi +exit 0 diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 00000000..787f0250 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,14 @@ +FROM node:18 + +COPY package.json /app/package.json +WORKDIR /app +RUN npm install + +COPY . /app +RUN npm run build + +FROM nginx:stable + +COPY nginx.conf /etc/nginx/nginx.conf + +COPY --from=0 /app/build /usr/share/nginx/html diff --git a/app/README.md b/app/README.md index 35b002db..b4278f23 100644 --- a/app/README.md +++ b/app/README.md @@ -1,6 +1,9 @@ -# Packit Front End +****# Packit Front End Interface is built with [React library](https://reactjs.org) +## Requirements +Node 18. + ## Available Scripts **App can be started in the project directory when you run:** @@ -32,3 +35,9 @@ Your app is ready to be deployed! See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. +## Building a docker image +The app is containerised into an image based on nginx. Note that in deployment this will be proxied, so no +security configuration is required here. +1. `./app/scripts/build` builds a docker image. +2. `./app/scripts/build-and-push` builds and pushes an image to dockerhub. This script is run on CI. +3. `./app/scripts/run` runs a built image with the current branch name.**** diff --git a/app/nginx.conf b/app/nginx.conf new file mode 100644 index 00000000..206c7bd5 --- /dev/null +++ b/app/nginx.conf @@ -0,0 +1,51 @@ +user nginx; +worker_processes 1; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + + keepalive_timeout 65; + + server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + + # Don't cache these files + location ~* \.(?:manifest|appcache|html?|xml|json)$ { + expires -1; + } + + # CSS, JS, and source map files (do cache these) + location ~* \.(?:css|js|map)$ { + try_files $uri =404; + expires -1; + access_log off; + add_header Cache-Control "public"; + } + + # Any route that doesn't have a file extension, and which doesn't exist on the + # sever, we assume is a route within the React app, and just map it to index.html + location / { + try_files $uri $uri/ /index.html; + } + } +} diff --git a/app/scripts/build b/app/scripts/build new file mode 100755 index 00000000..c62e8f18 --- /dev/null +++ b/app/scripts/build @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e +HERE=$(dirname $0) +. $HERE/common + +PACKAGE_ROOT=$(realpath $HERE/..) + +docker build \ + -t "$TAG_SHA" \ + -t "$TAG_BRANCH" \ + $PACKAGE_ROOT diff --git a/app/scripts/build-and-push b/app/scripts/build-and-push new file mode 100755 index 00000000..4b309491 --- /dev/null +++ b/app/scripts/build-and-push @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e + +HERE=$(dirname $0) +. "$HERE"/common + +# build docker image +. "$HERE"/build + +docker push $TAG_SHA +docker push $TAG_BRANCH diff --git a/app/scripts/common b/app/scripts/common new file mode 100755 index 00000000..db8cd9a3 --- /dev/null +++ b/app/scripts/common @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -ex + +if [[ -v "GITHUB_SHA" ]]; then + GIT_ID=${GITHUB_SHA:0:7} +else + GIT_ID=$(git rev-parse --short=7 HEAD) +fi + +if [[ -v "BRANCH_NAME" ]]; then + GIT_BRANCH=${BRANCH_NAME} +else + GIT_BRANCH=$(git symbolic-ref --short HEAD) +fi + +ORG=mrcide +IMAGE_NAME=packit +TAG_SHA="${ORG}/${IMAGE_NAME}:${GIT_ID}" +TAG_BRANCH="${ORG}/${IMAGE_NAME}:${GIT_BRANCH}" diff --git a/app/scripts/run b/app/scripts/run new file mode 100755 index 00000000..55c9c9e0 --- /dev/null +++ b/app/scripts/run @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -e + +HERE=$(dirname $0) +. $HERE/common + +docker run -d \ + --network=host \ + --name packit \ + "$TAG_BRANCH" diff --git a/app/scripts/smoke-test b/app/scripts/smoke-test new file mode 100755 index 00000000..719b330e --- /dev/null +++ b/app/scripts/smoke-test @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +wait_for() +{ + echo "waiting up to $TIMEOUT seconds for web app" + start_ts=$(date +%s) + for i in $(seq $TIMEOUT); do + + result=$(curl --write-out %{http_code} --silent --output /dev/null http://localhost:80) + if [[ $result -eq 200 ]]; then + end_ts=$(date +%s) + echo "Web app available after $((end_ts - start_ts)) seconds" + break + fi + sleep 1 + echo "...still waiting" + done + return $result +} + +if [[ -v "BRANCH_NAME" ]]; then + TAG=$BRANCH_NAME +else + TAG=$(git symbolic-ref --short HEAD) +fi + +docker run --network=host -d mrcide/packit:$TAG + +# The variable expansion below is 60s by default, or the argument provided +# to this script +TIMEOUT="${1:-60}" +wait_for +RESULT=$? +if [[ $RESULT -ne 200 ]]; then + echo "App did not become available in time" + exit 1 +fi +exit 0 diff --git a/scripts/clear-docker b/scripts/clear-docker new file mode 100755 index 00000000..36359e77 --- /dev/null +++ b/scripts/clear-docker @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +docker rm --force $(docker ps --all --quiet) || true +docker network prune --force +docker volume prune --force From 7f345834ccddcc89371ea7d5e60d7adfbe89960e Mon Sep 17 00:00:00 2001 From: Alex Hill Date: Thu, 18 May 2023 19:58:03 +0100 Subject: [PATCH 2/9] login to docker on CI --- .github/workflows/frontend-test-and-build.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/frontend-test-and-build.yml b/.github/workflows/frontend-test-and-build.yml index 86c7bdc6..00b26343 100644 --- a/.github/workflows/frontend-test-and-build.yml +++ b/.github/workflows/frontend-test-and-build.yml @@ -28,6 +28,15 @@ jobs: uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Installing dependencies run: npm ci --prefix=app - name: Building app From 1cbbc4e674c7edff013357a06ff1b3c88b56f293 Mon Sep 17 00:00:00 2001 From: Alex Hill Date: Mon, 22 May 2023 11:24:48 +0100 Subject: [PATCH 3/9] include dev and prod config --- .github/workflows/frontend-test-and-build.yml | 2 +- app/Dockerfile | 1 + app/scripts/smoke-test | 2 +- app/src/apiService.ts | 3 +- app/src/config/appConfig.ts | 19 ++++++++++ scripts/smoke-test.sh | 37 ------------------- 6 files changed, 24 insertions(+), 40 deletions(-) create mode 100644 app/src/config/appConfig.ts delete mode 100755 scripts/smoke-test.sh diff --git a/.github/workflows/frontend-test-and-build.yml b/.github/workflows/frontend-test-and-build.yml index 00b26343..b5ba325e 100644 --- a/.github/workflows/frontend-test-and-build.yml +++ b/.github/workflows/frontend-test-and-build.yml @@ -54,4 +54,4 @@ jobs: run: ./scripts/build-and-push - name: Smoke test working-directory: app - run: ./scripts/smoke-test.sh + run: ./scripts/smoke-test diff --git a/app/Dockerfile b/app/Dockerfile index 787f0250..ec5ee1b8 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -5,6 +5,7 @@ WORKDIR /app RUN npm install COPY . /app + RUN npm run build FROM nginx:stable diff --git a/app/scripts/smoke-test b/app/scripts/smoke-test index 719b330e..bb7f2741 100755 --- a/app/scripts/smoke-test +++ b/app/scripts/smoke-test @@ -5,7 +5,7 @@ wait_for() start_ts=$(date +%s) for i in $(seq $TIMEOUT); do - result=$(curl --write-out %{http_code} --silent --output /dev/null http://localhost:80) + result=$(curl --write-out %{http_code} --silent --output /dev/null http://localhost) if [[ $result -eq 200 ]]; then end_ts=$(date +%s) echo "Web app available after $((end_ts - start_ts)) seconds" diff --git a/app/src/apiService.ts b/app/src/apiService.ts index cfb6bfa3..92b9c5f6 100644 --- a/app/src/apiService.ts +++ b/app/src/apiService.ts @@ -5,8 +5,9 @@ import { AsyncThunk, SerializedError, } from "@reduxjs/toolkit"; import {RejectedErrorValue} from "./types"; +import config from "./config/appConfig"; -const baseURL = "http://localhost:8080"; +const baseURL = config.apiUrl(); interface CustomAsyncThunkOptions extends AsyncThunkOptions { rejectValue: SerializedError diff --git a/app/src/config/appConfig.ts b/app/src/config/appConfig.ts new file mode 100644 index 00000000..869c6f3e --- /dev/null +++ b/app/src/config/appConfig.ts @@ -0,0 +1,19 @@ +interface AppConfig { + apiUrl: () => string +} + +const devConfig: AppConfig = { + apiUrl: () => "http://localhost:8080" +}; + +const prodConfig: AppConfig = { + apiUrl: () => `https://${window.location.host}/packit/api`, +}; + +let config = devConfig; + +if (process.env.NODE_ENV == "production") { + config = prodConfig; +} + +export default config; diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh deleted file mode 100755 index 9c48048b..00000000 --- a/scripts/smoke-test.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -wait_for() -{ - echo "waiting up to $TIMEOUT seconds for API" - start_ts=$(date +%s) - for i in $(seq $TIMEOUT); do - - result=$(curl --write-out %{http_code} --silent --output /dev/null http://localhost:8080/packets) - if [[ $result -eq 200 ]]; then - end_ts=$(date +%s) - echo "API available after $((end_ts - start_ts)) seconds" - break - fi - sleep 1 - echo "...still waiting" - done - return $result -} - -if [[ -v "BRANCH_NAME" ]]; then - TAG=$BRANCH_NAME -else - TAG=$(git symbolic-ref --short HEAD) -fi - -docker run --network=host -d mrcide/packit-api:$TAG - -# The variable expansion below is 100s by default, or the argument provided -# to this script -TIMEOUT="${1:-60}" -wait_for -RESULT=$? -if [[ $RESULT -ne 200 ]]; then - echo "API did not become available in time" - exit 1 -fi -exit 0 From 4b08be60cec0ef4023ce8e9032346b2c0e8ce09a Mon Sep 17 00:00:00 2001 From: Alex Hill Date: Mon, 22 May 2023 12:06:44 +0100 Subject: [PATCH 4/9] add test --- app/README.md | 2 +- app/scripts/build-and-push | 1 - app/src/tests/appConfig.test.ts | 29 +++++++++++++++++++++++++++++ scripts/clear-docker | 1 - 4 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 app/src/tests/appConfig.test.ts diff --git a/app/README.md b/app/README.md index b4278f23..96952d3c 100644 --- a/app/README.md +++ b/app/README.md @@ -1,4 +1,4 @@ -****# Packit Front End +#Packit Front End Interface is built with [React library](https://reactjs.org) ## Requirements diff --git a/app/scripts/build-and-push b/app/scripts/build-and-push index 4b309491..695fd2e1 100755 --- a/app/scripts/build-and-push +++ b/app/scripts/build-and-push @@ -1,5 +1,4 @@ #!/usr/bin/env bash - set -e HERE=$(dirname $0) diff --git a/app/src/tests/appConfig.test.ts b/app/src/tests/appConfig.test.ts new file mode 100644 index 00000000..9b841ac5 --- /dev/null +++ b/app/src/tests/appConfig.test.ts @@ -0,0 +1,29 @@ +import config from "../config/appConfig"; + +describe("api service", () => { + + const OLD_ENV = process.env; + + beforeEach(() => { + jest.resetModules() // Most important - it clears the cache + process.env = { ...OLD_ENV }; // Make a copy + }); + + afterAll(() => { + process.env = OLD_ENV; // Restore old environment + }); + + test("uses default config by default", () => { + expect(config.apiUrl()).toBe("http://localhost:8080"); + }); + + test("uses production config if node_env is production", () => { + // Set the variables + // @ts-ignore + process.env.NODE_ENV = "production" + const config = require("../config/appConfig").default + + expect(config.apiUrl()).toBe("https://localhost/packit/api"); + }); +}); + diff --git a/scripts/clear-docker b/scripts/clear-docker index 36359e77..a3bad165 100755 --- a/scripts/clear-docker +++ b/scripts/clear-docker @@ -1,5 +1,4 @@ #!/usr/bin/env bash - set -euxo pipefail docker rm --force $(docker ps --all --quiet) || true From dc94eedc4b75a816544f21bcace44107cae22aa9 Mon Sep 17 00:00:00 2001 From: Alex Hill Date: Mon, 22 May 2023 12:13:46 +0100 Subject: [PATCH 5/9] lint --- app/src/tests/appConfig.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/tests/appConfig.test.ts b/app/src/tests/appConfig.test.ts index 9b841ac5..883d9f83 100644 --- a/app/src/tests/appConfig.test.ts +++ b/app/src/tests/appConfig.test.ts @@ -5,7 +5,7 @@ describe("api service", () => { const OLD_ENV = process.env; beforeEach(() => { - jest.resetModules() // Most important - it clears the cache + jest.resetModules(); // Important - it clears the cache process.env = { ...OLD_ENV }; // Make a copy }); @@ -18,11 +18,11 @@ describe("api service", () => { }); test("uses production config if node_env is production", () => { - // Set the variables + /* eslint-disable */ // @ts-ignore - process.env.NODE_ENV = "production" - const config = require("../config/appConfig").default - + process.env.NODE_ENV = "production"; + const config = require("../config/appConfig").default; + /* eslint-enable */ expect(config.apiUrl()).toBe("https://localhost/packit/api"); }); }); From 429737156f4d194ce5e87e89778419b1bf3ed6cb Mon Sep 17 00:00:00 2001 From: Alex Hill Date: Mon, 22 May 2023 14:27:35 +0100 Subject: [PATCH 6/9] homepage option --- app/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/app/package.json b/app/package.json index 31282fc8..10e5b5b5 100644 --- a/app/package.json +++ b/app/package.json @@ -52,6 +52,7 @@ "eject": "react-scripts eject", "serve": "serve -s build" }, + "homepage": ".", "eslintConfig": { "extends": [ "react-app", From 9c593022684874c66259cb316f6fc2d2c59da1ab Mon Sep 17 00:00:00 2001 From: Alex Hill Date: Wed, 24 May 2023 11:12:35 +0100 Subject: [PATCH 7/9] use npm ci --- app/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Dockerfile b/app/Dockerfile index ec5ee1b8..e149e2e1 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -1,8 +1,9 @@ FROM node:18 COPY package.json /app/package.json +COPY package-lock.json /app/package-lock.json WORKDIR /app -RUN npm install +RUN npm ci COPY . /app From 43af0d9545fd5f2c94099c11646bb2f5a885c0fb Mon Sep 17 00:00:00 2001 From: Alex Hill Date: Wed, 24 May 2023 12:45:29 +0100 Subject: [PATCH 8/9] asterisks --- app/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/README.md b/app/README.md index b54c5626..22568cb8 100644 --- a/app/README.md +++ b/app/README.md @@ -52,4 +52,4 @@ The app is containerised into an image based on nginx. Note that in deployment t security configuration is required here. 1. `./app/scripts/build` builds a docker image. 2. `./app/scripts/build-and-push` builds and pushes an image to dockerhub. This script is run on CI. -3. `./app/scripts/run` runs a built image with the current branch name.**** +3. `./app/scripts/run` runs a built image with the current branch name. From 4eb357dec80c0e1ba2e96cfd07ba0650db535205 Mon Sep 17 00:00:00 2001 From: Alex Hill Date: Wed, 24 May 2023 12:47:43 +0100 Subject: [PATCH 9/9] rename config -> appConfig --- app/src/apiService.ts | 4 ++-- app/src/config/appConfig.ts | 6 +++--- app/src/tests/appConfig.test.ts | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/apiService.ts b/app/src/apiService.ts index 92b9c5f6..fc54d17e 100644 --- a/app/src/apiService.ts +++ b/app/src/apiService.ts @@ -5,9 +5,9 @@ import { AsyncThunk, SerializedError, } from "@reduxjs/toolkit"; import {RejectedErrorValue} from "./types"; -import config from "./config/appConfig"; +import appConfig from "./config/appConfig"; -const baseURL = config.apiUrl(); +const baseURL = appConfig.apiUrl(); interface CustomAsyncThunkOptions extends AsyncThunkOptions { rejectValue: SerializedError diff --git a/app/src/config/appConfig.ts b/app/src/config/appConfig.ts index 869c6f3e..003b805b 100644 --- a/app/src/config/appConfig.ts +++ b/app/src/config/appConfig.ts @@ -10,10 +10,10 @@ const prodConfig: AppConfig = { apiUrl: () => `https://${window.location.host}/packit/api`, }; -let config = devConfig; +let appConfig = devConfig; if (process.env.NODE_ENV == "production") { - config = prodConfig; + appConfig = prodConfig; } -export default config; +export default appConfig; diff --git a/app/src/tests/appConfig.test.ts b/app/src/tests/appConfig.test.ts index 883d9f83..d5e37697 100644 --- a/app/src/tests/appConfig.test.ts +++ b/app/src/tests/appConfig.test.ts @@ -1,4 +1,4 @@ -import config from "../config/appConfig"; +import appConfig from "../config/appConfig"; describe("api service", () => { @@ -14,16 +14,16 @@ describe("api service", () => { }); test("uses default config by default", () => { - expect(config.apiUrl()).toBe("http://localhost:8080"); + expect(appConfig.apiUrl()).toBe("http://localhost:8080"); }); test("uses production config if node_env is production", () => { /* eslint-disable */ // @ts-ignore process.env.NODE_ENV = "production"; - const config = require("../config/appConfig").default; + const appConfig = require("../config/appConfig").default; /* eslint-enable */ - expect(config.apiUrl()).toBe("https://localhost/packit/api"); + expect(appConfig.apiUrl()).toBe("https://localhost/packit/api"); }); });