Skip to content

Commit

Permalink
ENVs integrity check (blockscout#1039)
Browse files Browse the repository at this point in the history
* group account envs in docs

* remove NEXT_PUBLIC_LOGOUT_RETURN_URL env

* simple ENVs checker

* add types for all envs

* group values in config

* global envs types

* text tweaks

* fixes

* [skip ci] fix docker build
  • Loading branch information
tom2drum authored Jul 26, 2023
1 parent b65d96a commit d69b809
Show file tree
Hide file tree
Showing 48 changed files with 2,096 additions and 109 deletions.
7 changes: 6 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
Dockerfile
.dockerignore
node_modules
/**/node_modules
node_modules_linux
npm-debug.log
README.md
.next
.git
.git
*.tsbuildinfo
.eslintcache
/test-results/
/playwright-report/
1 change: 0 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ NEXT_PUBLIC_OTHER_LINKS=__PLACEHOLDER_FOR_NEXT_PUBLIC_OTHER_LINKS__
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_CONFIG_URL__
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM__
NEXT_PUBLIC_LOGOUT_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_URL__
NEXT_PUBLIC_LOGOUT_RETURN_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_RETURN_URL__
NEXT_PUBLIC_HOMEPAGE_CHARTS=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_CHARTS__
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR__
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND__
Expand Down
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ module.exports = {
},
},
{
files: [ 'configs/**/*.js', 'configs/**/*.ts', '*.config.ts', 'playwright/**/*.ts' ],
files: [ 'configs/**/*.js', 'configs/**/*.ts', '*.config.ts', 'playwright/**/*.ts', 'deploy/tools/**/*.ts' ],
rules: {
// for configs allow to consume env variables from process.env directly
'no-restricted-properties': [ 0 ],
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# dependencies
/node_modules
/node_modules_linux
/**/node_modules
/.pnp
.pnp.js

Expand Down Expand Up @@ -48,3 +49,8 @@ yarn-error.log*
/playwright/envs.js

**.dec**

# tools: envs-validator
/deploy/tools/envs-validator/index.js
/deploy/tools/envs-validator/envs.ts
/deploy/tools/envs-validator/schema.ts
36 changes: 26 additions & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,33 +1,49 @@
# Install dependencies only when needed
# *****************************
# *** STAGE 1: Dependencies ***
# *****************************
FROM node:18-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
# Install dependencies for App
WORKDIR /app
COPY package.json yarn.lock ./
RUN apk add git
RUN yarn --frozen-lockfile

# Rebuild the source code only when needed
# Install dependencies for ENVs checker
WORKDIR /envs-validator
COPY ./deploy/tools/envs-validator/package.json ./deploy/tools/envs-validator/yarn.lock ./
RUN yarn --frozen-lockfile

# *****************************
# ****** STAGE 2: Build *******
# *****************************
FROM node:18-alpine AS builder

# Build app for production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
COPY .env.template .env.production
RUN rm -rf ./deploy/tools/envs-validator

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn build

ARG SENTRY_DSN
ARG NEXT_PUBLIC_SENTRY_DSN
ARG SENTRY_CSP_REPORT_URI
ARG SENTRY_AUTH_TOKEN

# Build ENVs checker
WORKDIR /envs-validator
COPY --from=deps /envs-validator/node_modules ./node_modules
COPY ./deploy/tools/envs-validator .
COPY ./types/envs.ts .
RUN yarn build

# *****************************
# ******* STAGE 3: Run ********
# *****************************
# Production image, copy all the files and run next
FROM node:18-alpine AS runner
WORKDIR /app
Expand All @@ -50,6 +66,7 @@ RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /envs-validator/index.js ./envs-validator.js

# Copy scripts and ENV templates file
COPY ./deploy/scripts/entrypoint.sh .
Expand All @@ -61,7 +78,6 @@ COPY .env.template .
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

# Execute script for replace build ENV with run ones
RUN apk add --no-cache --upgrade bash
RUN ["chmod", "+x", "./entrypoint.sh"]
RUN ["chmod", "+x", "./replace_envs.sh"]
Expand Down
45 changes: 28 additions & 17 deletions configs/app/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { ChainIndicatorId } from 'ui/home/indicators/types';

import stripTrailingSlash from 'lib/stripTrailingSlash';

const getEnvValue = (env: string | undefined) => env?.replaceAll('\'', '"');
const getEnvValue = <T extends string>(env: T | undefined): T | undefined => env?.replaceAll('\'', '"') as T;
const parseEnvJson = <DataType>(env: string | undefined): DataType | null => {
try {
return JSON.parse(env || 'null') as DataType | null;
Expand Down Expand Up @@ -70,8 +70,9 @@ const logoutUrl = (() => {
try {
const envUrl = getEnvValue(process.env.NEXT_PUBLIC_LOGOUT_URL);
const auth0ClientId = getEnvValue(process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID);
const returnUrl = getEnvValue(process.env.NEXT_PUBLIC_LOGOUT_RETURN_URL);
if (!envUrl || !auth0ClientId || !returnUrl) {
const returnUrl = authUrl + '/auth/logout';

if (!envUrl || !auth0ClientId) {
throw Error();
}

Expand Down Expand Up @@ -113,20 +114,30 @@ const config = Object.freeze({
rpcUrl: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_RPC_URL),
isTestnet: getEnvValue(process.env.NEXT_PUBLIC_IS_TESTNET) === 'true',
},
otherLinks: parseEnvJson<Array<NavItemExternal>>(getEnvValue(process.env.NEXT_PUBLIC_OTHER_LINKS)) || [],
featuredNetworks: getEnvValue(process.env.NEXT_PUBLIC_FEATURED_NETWORKS),
footerLinks: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_LINKS),
frontendVersion: getEnvValue(process.env.NEXT_PUBLIC_GIT_TAG),
frontendCommit: getEnvValue(process.env.NEXT_PUBLIC_GIT_COMMIT_SHA),
isAccountSupported: getEnvValue(process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED) === 'true',
marketplaceConfigUrl: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_CONFIG_URL),
marketplaceSubmitForm: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM),
protocol: appSchema,
host: appHost,
port: appPort,
baseUrl,
authUrl,
logoutUrl,
navigation: {
otherLinks: parseEnvJson<Array<NavItemExternal>>(getEnvValue(process.env.NEXT_PUBLIC_OTHER_LINKS)) || [],
featuredNetworks: getEnvValue(process.env.NEXT_PUBLIC_FEATURED_NETWORKS),
},
footer: {
links: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_LINKS),
frontendVersion: getEnvValue(process.env.NEXT_PUBLIC_GIT_TAG),
frontendCommit: getEnvValue(process.env.NEXT_PUBLIC_GIT_COMMIT_SHA),
},
marketplace: {
configUrl: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_CONFIG_URL),
submitForm: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM),
},
account: {
isEnabled: getEnvValue(process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED) === 'true',
authUrl,
logoutUrl,
},
app: {
protocol: appSchema,
host: appHost,
port: appPort,
baseUrl,
},
ad: {
adBannerProvider: getAdBannerProvider(),
adTextProvider: getAdTextProvider(),
Expand Down
4 changes: 2 additions & 2 deletions configs/envs/.env.poa_core
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ NEXT_PUBLIC_NETWORK_CURRENCY_NAME=POA
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=POA
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0x029a799563238d0e75e20be2f4bda0ea68d00172
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
#NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://core.poa.network
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
#NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C

# api config
NEXT_PUBLIC_API_HOST=blockscout.com
Expand Down
7 changes: 7 additions & 0 deletions deploy/scripts/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
#!/bin/bash

# Check run-time ENVs values integrity
node "$(dirname "$0")/envs-validator.js" "$input"
if [ $? != 0 ]; then
echo ENV integrity check failed. 1>&2 && exit 1
fi

# Execute script for replace build-time ENVs placeholders with their values at runtime
./replace_envs.sh

echo "starting Nextjs"
Expand Down
30 changes: 30 additions & 0 deletions deploy/tools/envs-validator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* eslint-disable no-console */
import type { ZodError } from 'zod-validation-error';
import { fromZodError } from 'zod-validation-error';

import { nextPublicEnvsSchema } from './schema';

try {
const appEnvs = Object.entries(process.env)
.filter(([ key ]) => key.startsWith('NEXT_PUBLIC_'))
.reduce((result, [ key, value ]) => {
result[key] = value || '';
return result;
}, {} as Record<string, string>);

console.log(`⏳ Validating environment variables schema...`);
nextPublicEnvsSchema.parse(appEnvs);
console.log('👍 All good!\n');
} catch (error) {
const validationError = fromZodError(
error as ZodError,
{
prefix: '',
prefixSeparator: '\n ',
issueSeparator: ';\n ',
},
);
console.log(validationError);
console.log('🚨 ENV set is invalid\n');
process.exit(1);
}
22 changes: 22 additions & 0 deletions deploy/tools/envs-validator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "envs-validator",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "yarn ts-to-zod ./envs.ts ./schema.ts && yarn webpack-cli -c ./webpack.config.js",
"validate": "node ./index.js",
"dev": "cp ../../../types/envs.ts ./ && yarn build && yarn dotenv -e ../../../configs/envs/.env.poa_core yarn validate"
},
"dependencies": {
"ts-loader": "^9.4.4",
"ts-to-zod": "^3.1.3",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"zod": "^3.21.4",
"zod-validation-error": "^1.3.1"
},
"devDependencies": {
"dotenv-cli": "^7.2.1"
}
}
15 changes: 15 additions & 0 deletions deploy/tools/envs-validator/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es6",
"skipLibCheck": true,
"strict": true,
"esModuleInterop": true,
"module": "CommonJS",
"moduleResolution": "node",
"isolatedModules": true,
"incremental": true,
"baseUrl": "."
},
"include": ["./schema.ts"],
"exclude": ["node_modules"]
}
21 changes: 21 additions & 0 deletions deploy/tools/envs-validator/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const path = require('path');
module.exports = {
mode: 'production',
entry: path.resolve(__dirname) + '/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
output: {
filename: 'index.js',
path: path.resolve(__dirname),
},
};
Loading

0 comments on commit d69b809

Please sign in to comment.