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] on: [workflow_dispatch]
jobs: jobs:
backend-image: backend-image:
name: Build backend image name: Build backend image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - name: ☁️ Checkout source
name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
- - name: 🔧 Set up QEMU
name: 🔧 Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
- - name: 🔧 Set up Docker Buildx
name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- - name: 🐋 Login to Docker Hub
name: 🐋 Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
# - - name: 📦 Build backend and export to Docker
# name: 📦 Build backend and export to Docker uses: docker/build-push-action@v3
# uses: docker/build-push-action@v3 with:
# with: load: true
# load: true context: backend
# context: backend tags: infisical/backend:test
# tags: infisical/backend:test - name: ⏻ Spawn backend container and dependencies
# - run: |
# name: 🧪 Test backend image docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
# run: | - name: 🧪 Test backend image
# docker run --rm infisical/backend:test run: |
- ./.github/resources/healthcheck.sh infisical-backend-test
name: 🏗️ Build backend and push - 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 uses: docker/build-push-action@v3
with: with:
push: true push: true
@@ -44,42 +42,40 @@ jobs:
tags: infisical/backend:latest tags: infisical/backend:latest
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
frontend-image: frontend-image:
name: Build frontend image name: Build frontend image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - name: ☁️ Checkout source
name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
- - name: 🔧 Set up QEMU
name: 🔧 Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
- - name: 🔧 Set up Docker Buildx
name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- - name: 🐋 Login to Docker Hub
name: 🐋 Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
# - - name: 📦 Build frontend and export to Docker
# name: 📦 Build frontend and export to Docker uses: docker/build-push-action@v3
# uses: docker/build-push-action@v3 with:
# with: load: true
# load: true context: frontend
# context: frontend tags: infisical/frontend:test
# tags: infisical/frontend:test build-args: |
# build-args: | POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
# POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} - name: ⏻ Spawn frontend container
# - run: |
# name: 🧪 Test frontend image docker run -d --rm --name infisical-frontend-test infisical/frontend:test
# run: | - name: 🧪 Test frontend image
# docker run --rm infisical/frontend:test run: |
- ./.github/resources/healthcheck.sh infisical-frontend-test
name: 🏗️ Build frontend and push - name: ⏻ Shut down frontend container
run: |
docker stop infisical-frontend-test
- name: 🏗️ Build frontend and push
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
with: with:
push: true push: true

View File

@@ -2,11 +2,14 @@ FROM node:16-bullseye-slim
WORKDIR /app WORKDIR /app
COPY package*.json . COPY package.json package-lock.json ./
RUN npm install RUN npm ci --only-production
COPY . . 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", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@godaddy/terminus": "^4.11.2",
"@sentry/node": "^7.14.0", "@sentry/node": "^7.14.0",
"@sentry/tracing": "^7.19.0", "@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
@@ -2028,6 +2029,14 @@
"url": "https://opencollective.com/eslint" "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": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.7", "version": "0.11.7",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz",
@@ -6731,7 +6740,129 @@
"treeverse", "treeverse",
"validate-npm-package-name", "validate-npm-package-name",
"which", "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, "dev": true,
"dependencies": { "dependencies": {
@@ -10192,6 +10323,15 @@
"node": ">= 0.8" "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": { "node_modules/strict-uri-encode": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", "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" "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": { "@humanwhocodes/config-array": {
"version": "0.11.7", "version": "0.11.7",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", "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", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" "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": { "strict-uri-encode": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",

View File

@@ -1,5 +1,6 @@
{ {
"dependencies": { "dependencies": {
"@godaddy/terminus": "^4.11.2",
"@sentry/node": "^7.14.0", "@sentry/node": "^7.14.0",
"@sentry/tracing": "^7.19.0", "@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1", "@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 express from 'express';
import helmet from 'helmet'; import helmet from 'helmet';
import cors from 'cors'; import cors from 'cors';
@@ -7,63 +9,67 @@ import dotenv from 'dotenv';
dotenv.config(); dotenv.config();
import * as Sentry from '@sentry/node'; 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 { apiLimiter } from './helpers/rateLimiter';
import { createTerminus } from '@godaddy/terminus';
const app = express(); const app = express();
Sentry.init({ Sentry.init({
dsn: SENTRY_DSN, dsn: SENTRY_DSN,
tracesSampleRate: 1.0, tracesSampleRate: 1.0,
debug: NODE_ENV === 'production' ? false : true, debug: NODE_ENV === 'production' ? false : true,
environment: NODE_ENV environment: NODE_ENV
}); });
import { import {
signup as signupRouter, signup as signupRouter,
auth as authRouter, auth as authRouter,
organization as organizationRouter, organization as organizationRouter,
workspace as workspaceRouter, workspace as workspaceRouter,
membershipOrg as membershipOrgRouter, membershipOrg as membershipOrgRouter,
membership as membershipRouter, membership as membershipRouter,
key as keyRouter, key as keyRouter,
inviteOrg as inviteOrgRouter, inviteOrg as inviteOrgRouter,
user as userRouter, user as userRouter,
userAction as userActionRouter, userAction as userActionRouter,
secret as secretRouter, secret as secretRouter,
serviceToken as serviceTokenRouter, serviceToken as serviceTokenRouter,
password as passwordRouter, password as passwordRouter,
stripe as stripeRouter, stripe as stripeRouter,
integration as integrationRouter, integration as integrationRouter,
integrationAuth as integrationAuthRouter integrationAuth as integrationAuthRouter
} from './routes'; } from './routes';
const connectWithRetry = () => { const connectWithRetry = () => {
mongoose.connect(MONGO_URL) mongoose
.then(() => console.log('Successfully connected to DB')) .connect(MONGO_URL)
.catch((e) => { .then(() => console.log('Successfully connected to DB'))
console.log('Failed to connect to DB ', e); .catch((e) => {
setTimeout(() => { console.log('Failed to connect to DB ', e);
console.log(e); setTimeout(() => {
}, 5000); console.log(e);
}); }, 5000);
} });
};
connectWithRetry(); connectWithRetry();
app.enable('trust proxy'); app.enable('trust proxy');
app.use(cookieParser()); app.use(cookieParser());
app.use(cors({ app.use(
credentials: true, cors({
origin: SITE_URL credentials: true,
})); origin: SITE_URL
})
);
if (NODE_ENV === 'production') { if (NODE_ENV === 'production') {
// enable app-wide rate-limiting + helmet security // enable app-wide rate-limiting + helmet security
// in production // in production
app.disable('x-powered-by'); app.disable('x-powered-by');
app.use(apiLimiter); app.use(apiLimiter);
app.use(helmet()); app.use(helmet());
} }
app.use(express.json()); 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', integrationRouter);
app.use('/api/v1/integration-auth', integrationAuthRouter); app.use('/api/v1/integration-auth', integrationAuthRouter);
app.listen(PORT, () => { const server = http.createServer(app);
console.log('Listening on PORT ' + PORT);
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

@@ -60,5 +60,8 @@ EXPOSE 3000
ENV PORT 3000 ENV PORT 3000
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED 1
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node scripts/healthcheck.js
CMD ["/app/scripts/start.sh"] 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();