From 752ebaa3ebfeb779734df9b6bb421fe5c8d8a969 Mon Sep 17 00:00:00 2001 From: Reginald Bondoc Date: Sun, 11 Dec 2022 22:36:46 +0100 Subject: [PATCH] Add healthchecks and test image before push --- .github/resources/docker-compose.be-test.yml | 30 ++++ .github/resources/healthcheck.sh | 26 ++++ .github/workflows/docker-image.yml | 88 +++++------ backend/Dockerfile | 9 +- backend/healthcheck.js | 27 ++++ backend/package-lock.json | 155 ++++++++++++++++++- backend/package.json | 1 + backend/src/index.ts | 113 +++++++++----- docker-compose.yml | 4 +- frontend/Dockerfile | 3 + frontend/scripts/healthcheck.js | 26 ++++ 11 files changed, 389 insertions(+), 93 deletions(-) create mode 100644 .github/resources/docker-compose.be-test.yml create mode 100755 .github/resources/healthcheck.sh create mode 100644 backend/healthcheck.js create mode 100644 frontend/scripts/healthcheck.js diff --git a/.github/resources/docker-compose.be-test.yml b/.github/resources/docker-compose.be-test.yml new file mode 100644 index 0000000000..6efdd87f64 --- /dev/null +++ b/.github/resources/docker-compose.be-test.yml @@ -0,0 +1,30 @@ +version: '3' + +services: + backend: + container_name: infisical-backend-test + restart: unless-stopped + depends_on: + - mongo + image: infisical/backend:test + command: npm run start + environment: + - NODE_ENV=production + - MONGO_URL=mongodb://test:example@mongo:27017/?authSource=admin + - MONGO_USERNAME=test + - MONGO_PASSWORD=example + networks: + - infisical-test + + mongo: + container_name: infisical-mongo-test + image: mongo + restart: always + environment: + - MONGO_INITDB_ROOT_USERNAME=test + - MONGO_INITDB_ROOT_PASSWORD=example + networks: + - infisical-test + +networks: + infisical-test: diff --git a/.github/resources/healthcheck.sh b/.github/resources/healthcheck.sh new file mode 100755 index 0000000000..bc28e36071 --- /dev/null +++ b/.github/resources/healthcheck.sh @@ -0,0 +1,26 @@ +# Name of the target container to check +container_name="$1" +# Timeout in seconds. Default: 60 +timeout=$((${2:-60})); + +if [ -z $container_name ]; then + echo "No container name specified"; + exit 1; +fi + +echo "Container: $container_name"; +echo "Timeout: $timeout sec"; + +try=0; +is_healthy="false"; +while [ $is_healthy != "true" ]; +do + try=$(($try + 1)); + printf "■"; + is_healthy=$(docker inspect --format='{{json .State.Health}}' $container_name | jq '.Status == "healthy"'); + sleep 1; + if [[ $try -eq $timeout ]]; then + echo " Container was not ready within timeout"; + exit 1; + fi +done diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index e85ddcfd56..1e836a1782 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -3,40 +3,38 @@ name: Push to Docker Hub on: [workflow_dispatch] jobs: - backend-image: name: Build backend image runs-on: ubuntu-latest steps: - - - name: ☁️ Checkout source + - name: ☁️ Checkout source uses: actions/checkout@v3 - - - name: 🔧 Set up QEMU + - name: 🔧 Set up QEMU uses: docker/setup-qemu-action@v2 - - - name: 🔧 Set up Docker Buildx + - name: 🔧 Set up Docker Buildx uses: docker/setup-buildx-action@v2 - - - name: 🐋 Login to Docker Hub + - name: 🐋 Login to Docker Hub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - # - - # name: 📦 Build backend and export to Docker - # uses: docker/build-push-action@v3 - # with: - # load: true - # context: backend - # tags: infisical/backend:test - # - - # name: 🧪 Test backend image - # run: | - # docker run --rm infisical/backend:test - - - name: 🏗️ Build backend and push + - name: 📦 Build backend and export to Docker + uses: docker/build-push-action@v3 + with: + load: true + context: backend + tags: infisical/backend:test + - name: ⏻ Spawn backend container and dependencies + run: | + docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull + - name: 🧪 Test backend image + run: | + ./.github/resources/healthcheck.sh infisical-backend-test + - name: ⏻ Shut down backend container and dependencies + run: | + docker compose -f .github/resources/docker-compose.be-test.yml down + - name: 🏗️ Build backend and push uses: docker/build-push-action@v3 with: push: true @@ -44,42 +42,40 @@ jobs: tags: infisical/backend:latest platforms: linux/amd64,linux/arm64 - frontend-image: name: Build frontend image runs-on: ubuntu-latest steps: - - - name: ☁️ Checkout source + - name: ☁️ Checkout source uses: actions/checkout@v3 - - - name: 🔧 Set up QEMU + - name: 🔧 Set up QEMU uses: docker/setup-qemu-action@v2 - - - name: 🔧 Set up Docker Buildx + - name: 🔧 Set up Docker Buildx uses: docker/setup-buildx-action@v2 - - - name: 🐋 Login to Docker Hub + - name: 🐋 Login to Docker Hub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - # - - # name: 📦 Build frontend and export to Docker - # uses: docker/build-push-action@v3 - # with: - # load: true - # context: frontend - # tags: infisical/frontend:test - # build-args: | - # POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} - # - - # name: 🧪 Test frontend image - # run: | - # docker run --rm infisical/frontend:test - - - name: 🏗️ Build frontend and push + - name: 📦 Build frontend and export to Docker + uses: docker/build-push-action@v3 + with: + load: true + context: frontend + tags: infisical/frontend:test + build-args: | + POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} + - name: ⏻ Spawn frontend container + run: | + docker run -d --rm --name infisical-frontend-test infisical/frontend:test + - name: 🧪 Test frontend image + run: | + ./.github/resources/healthcheck.sh infisical-frontend-test + - name: ⏻ Shut down frontend container + run: | + docker stop infisical-frontend-test + - name: 🏗️ Build frontend and push uses: docker/build-push-action@v3 with: push: true diff --git a/backend/Dockerfile b/backend/Dockerfile index 2cc2709476..ccc76e66e5 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,11 +2,14 @@ FROM node:16-bullseye-slim WORKDIR /app -COPY package*.json . +COPY package.json package-lock.json ./ -RUN npm install +RUN npm ci --only-production COPY . . -CMD ["npm", "run", "start"] +HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ + CMD node healthcheck.js + +CMD ["npm", "run", "start"] diff --git a/backend/healthcheck.js b/backend/healthcheck.js new file mode 100644 index 0000000000..7b798eb0a8 --- /dev/null +++ b/backend/healthcheck.js @@ -0,0 +1,27 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable no-console */ +/* eslint-disable no-undef */ +const http = require('http'); +const PORT = process.env.PORT || 4000; +const options = { + host: 'localhost', + port: PORT, + timeout: 2000, + path: '/healthcheck' +}; + +const healthCheck = http.request(options, (res) => { + console.log(`HEALTHCHECK STATUS: ${res.statusCode}`); + if (res.statusCode == 200) { + process.exit(0); + } else { + process.exit(1); + } +}); + +healthCheck.on('error', function (err) { + console.error(err); + process.exit(1); +}); + +healthCheck.end(); diff --git a/backend/package-lock.json b/backend/package-lock.json index d4c3d36a6b..c13f138d4c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@godaddy/terminus": "^4.11.2", "@sentry/node": "^7.14.0", "@sentry/tracing": "^7.19.0", "@types/crypto-js": "^4.1.1", @@ -2028,6 +2029,14 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@godaddy/terminus": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@godaddy/terminus/-/terminus-4.11.2.tgz", + "integrity": "sha512-e/kbOWpGKME42eltM/wXM3RxSUOrfureZxEd6Dt6NXyFoJ7E8lnmm7znXydJsL3B7ky4HRFZI+eHrep54NZbeQ==", + "dependencies": { + "stoppable": "^1.1.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", @@ -6731,7 +6740,129 @@ "treeverse", "validate-npm-package-name", "which", - "write-file-atomic" + "write-file-atomic", + "@colors/colors", + "@gar/promisify", + "@npmcli/disparity-colors", + "@npmcli/git", + "@npmcli/installed-package-contents", + "@npmcli/metavuln-calculator", + "@npmcli/move-file", + "@npmcli/name-from-folder", + "@npmcli/node-gyp", + "@npmcli/promise-spawn", + "@npmcli/query", + "@tootallnate/once", + "agent-base", + "agentkeepalive", + "aggregate-error", + "ansi-regex", + "ansi-styles", + "aproba", + "are-we-there-yet", + "asap", + "balanced-match", + "bin-links", + "binary-extensions", + "brace-expansion", + "builtins", + "cidr-regex", + "clean-stack", + "clone", + "cmd-shim", + "color-convert", + "color-name", + "color-support", + "common-ancestor-path", + "concat-map", + "console-control-strings", + "cssesc", + "debug", + "debuglog", + "defaults", + "delegates", + "depd", + "dezalgo", + "diff", + "emoji-regex", + "encoding", + "env-paths", + "err-code", + "fs.realpath", + "function-bind", + "gauge", + "has", + "has-flag", + "has-unicode", + "http-cache-semantics", + "http-proxy-agent", + "https-proxy-agent", + "humanize-ms", + "iconv-lite", + "ignore-walk", + "imurmurhash", + "indent-string", + "infer-owner", + "inflight", + "inherits", + "ip", + "ip-regex", + "is-core-module", + "is-fullwidth-code-point", + "is-lambda", + "isexe", + "json-stringify-nice", + "jsonparse", + "just-diff", + "just-diff-apply", + "lru-cache", + "minipass-collect", + "minipass-fetch", + "minipass-flush", + "minipass-json-stream", + "minipass-sized", + "minizlib", + "mute-stream", + "negotiator", + "normalize-package-data", + "npm-bundled", + "npm-normalize-package-bin", + "npm-packlist", + "once", + "path-is-absolute", + "postcss-selector-parser", + "promise-all-reject-late", + "promise-call-limit", + "promise-inflight", + "promise-retry", + "promzard", + "read-cmd-shim", + "readable-stream", + "retry", + "safe-buffer", + "safer-buffer", + "set-blocking", + "signal-exit", + "smart-buffer", + "socks", + "socks-proxy-agent", + "spdx-correct", + "spdx-exceptions", + "spdx-expression-parse", + "spdx-license-ids", + "string_decoder", + "string-width", + "strip-ansi", + "supports-color", + "unique-filename", + "unique-slug", + "util-deprecate", + "validate-npm-package-license", + "walk-up-path", + "wcwidth", + "wide-align", + "wrappy", + "yallist" ], "dev": true, "dependencies": { @@ -10192,6 +10323,15 @@ "node": ">= 0.8" } }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, "node_modules/strict-uri-encode": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", @@ -12607,6 +12747,14 @@ "strip-json-comments": "^3.1.1" } }, + "@godaddy/terminus": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@godaddy/terminus/-/terminus-4.11.2.tgz", + "integrity": "sha512-e/kbOWpGKME42eltM/wXM3RxSUOrfureZxEd6Dt6NXyFoJ7E8lnmm7znXydJsL3B7ky4HRFZI+eHrep54NZbeQ==", + "requires": { + "stoppable": "^1.1.0" + } + }, "@humanwhocodes/config-array": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", @@ -18631,6 +18779,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, + "stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==" + }, "strict-uri-encode": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 7e0dc7ad9b..32ca68b192 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,5 +1,6 @@ { "dependencies": { + "@godaddy/terminus": "^4.11.2", "@sentry/node": "^7.14.0", "@sentry/tracing": "^7.19.0", "@types/crypto-js": "^4.1.1", diff --git a/backend/src/index.ts b/backend/src/index.ts index a5ae449695..f9789c04d0 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-console */ +import http from 'http'; import express from 'express'; import helmet from 'helmet'; import cors from 'cors'; @@ -7,63 +9,67 @@ import dotenv from 'dotenv'; dotenv.config(); import * as Sentry from '@sentry/node'; -import { PORT, SENTRY_DSN, NODE_ENV, MONGO_URL, SITE_URL, POSTHOG_PROJECT_API_KEY, POSTHOG_HOST, TELEMETRY_ENABLED } from './config'; +import { PORT, SENTRY_DSN, NODE_ENV, MONGO_URL, SITE_URL } from './config'; import { apiLimiter } from './helpers/rateLimiter'; +import { createTerminus } from '@godaddy/terminus'; const app = express(); Sentry.init({ - dsn: SENTRY_DSN, - tracesSampleRate: 1.0, - debug: NODE_ENV === 'production' ? false : true, - environment: NODE_ENV + dsn: SENTRY_DSN, + tracesSampleRate: 1.0, + debug: NODE_ENV === 'production' ? false : true, + environment: NODE_ENV }); import { - signup as signupRouter, - auth as authRouter, - organization as organizationRouter, - workspace as workspaceRouter, - membershipOrg as membershipOrgRouter, - membership as membershipRouter, - key as keyRouter, - inviteOrg as inviteOrgRouter, - user as userRouter, - userAction as userActionRouter, - secret as secretRouter, - serviceToken as serviceTokenRouter, - password as passwordRouter, - stripe as stripeRouter, - integration as integrationRouter, - integrationAuth as integrationAuthRouter + signup as signupRouter, + auth as authRouter, + organization as organizationRouter, + workspace as workspaceRouter, + membershipOrg as membershipOrgRouter, + membership as membershipRouter, + key as keyRouter, + inviteOrg as inviteOrgRouter, + user as userRouter, + userAction as userActionRouter, + secret as secretRouter, + serviceToken as serviceTokenRouter, + password as passwordRouter, + stripe as stripeRouter, + integration as integrationRouter, + integrationAuth as integrationAuthRouter } from './routes'; const connectWithRetry = () => { - mongoose.connect(MONGO_URL) - .then(() => console.log('Successfully connected to DB')) - .catch((e) => { - console.log('Failed to connect to DB ', e); - setTimeout(() => { - console.log(e); - }, 5000); - }); -} + mongoose + .connect(MONGO_URL) + .then(() => console.log('Successfully connected to DB')) + .catch((e) => { + console.log('Failed to connect to DB ', e); + setTimeout(() => { + console.log(e); + }, 5000); + }); +}; connectWithRetry(); app.enable('trust proxy'); app.use(cookieParser()); -app.use(cors({ - credentials: true, - origin: SITE_URL -})); +app.use( + cors({ + credentials: true, + origin: SITE_URL + }) +); if (NODE_ENV === 'production') { - // enable app-wide rate-limiting + helmet security - // in production - app.disable('x-powered-by'); - app.use(apiLimiter); - app.use(helmet()); + // enable app-wide rate-limiting + helmet security + // in production + app.disable('x-powered-by'); + app.use(apiLimiter); + app.use(helmet()); } app.use(express.json()); @@ -86,6 +92,31 @@ app.use('/api/v1/stripe', stripeRouter); app.use('/api/v1/integration', integrationRouter); app.use('/api/v1/integration-auth', integrationAuthRouter); -app.listen(PORT, () => { - console.log('Listening on PORT ' + PORT); +const server = http.createServer(app); + +const onSignal = () => { + console.log('Server is starting clean-up'); + return Promise.all([ + // your clean logic, like closing database connections + ]); +}; + +const healthCheck = () => { + // `state.isShuttingDown` (boolean) shows whether the server is shutting down or not + return Promise + .resolve + // optionally include a resolve value to be included as + // info in the health check response + (); +}; + +createTerminus(server, { + healthChecks: { + '/healthcheck': healthCheck, + onSignal + } +}); + +server.listen(PORT, () => { + console.log('Listening on PORT ' + PORT); }); diff --git a/docker-compose.yml b/docker-compose.yml index cc92e14063..bd9022cef8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: - backend networks: - infisical - + backend: container_name: infisical-backend restart: unless-stopped @@ -28,7 +28,7 @@ services: - NODE_ENV=production networks: - infisical - + frontend: container_name: infisical-frontend restart: unless-stopped diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 5e59c68aad..5deb468f74 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -60,5 +60,8 @@ EXPOSE 3000 ENV PORT 3000 ENV NEXT_TELEMETRY_DISABLED 1 +HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ + CMD node scripts/healthcheck.js + CMD ["/app/scripts/start.sh"] diff --git a/frontend/scripts/healthcheck.js b/frontend/scripts/healthcheck.js new file mode 100644 index 0000000000..b29d70f44d --- /dev/null +++ b/frontend/scripts/healthcheck.js @@ -0,0 +1,26 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable no-console */ +/* eslint-disable no-undef */ +const http = require('http'); +const options = { + host: 'localhost', + port: 3000, + timeout: 2000, + path: '/' +}; + +const healthCheck = http.request(options, (res) => { + console.log(`HEALTHCHECK STATUS: ${res.statusCode}`); + if (res.statusCode == 200) { + process.exit(0); + } else { + process.exit(1); + } +}); + +healthCheck.on('error', function (err) { + console.error(err); + process.exit(1); +}); + +healthCheck.end();