Add healthchecks and test image before push

This commit is contained in:
Reginald Bondoc
2022-12-11 22:36:46 +01:00
parent 8896e1232b
commit 752ebaa3eb
11 changed files with 389 additions and 93 deletions

View File

@ -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:

26
.github/resources/healthcheck.sh vendored Executable file
View File

@ -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

View File

@ -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

View File

@ -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"]

27
backend/healthcheck.js Normal file
View File

@ -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();

View File

@ -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",

View File

@ -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",

View File

@ -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);
});

View File

@ -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

View File

@ -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"]

View File

@ -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();