feat: Configuration checking on backend startup, and via cli.

This commit is contained in:
jamesread 2024-09-06 23:34:26 +01:00
parent 11de46f5b3
commit d13c71d1f6
4 changed files with 168 additions and 1 deletions

View File

@ -8,6 +8,7 @@ import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SubscriptionExceptionFilter } from '@gitroom/backend/services/auth/permissions/subscription.exception';
import { HttpExceptionFilter } from '@gitroom/nestjs-libraries/services/exception.filter';
import { ConfigurationChecker } from '@gitroom/helpers/configuration/configuration.checker';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
@ -36,7 +37,26 @@ async function bootstrap() {
const port = process.env.PORT || 3000;
await app.listen(port);
checkConfiguration() // Do this last, so that users will see obvious issues at the end of the startup log without having to scroll up.
Logger.log(`🚀 Application is running on: http://localhost:${port}`);
}
function checkConfiguration() {
const checker = new ConfigurationChecker();
checker.readEnvFromProcess()
checker.check()
if (checker.hasIssues()) {
for (const issue of checker.getIssues()) {
Logger.warn(issue, 'Configuration issue')
}
Logger.warn("Configuration issues found: " + checker.getIssuesCount() + ". You run run `npm run command config:check` to quickly check again.")
} else {
Logger.log("Configuration check completed without any issues.")
}
}
bootstrap();

View File

@ -4,11 +4,12 @@ import { CheckStars } from './tasks/check.stars';
import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/database.module';
import { RefreshTokens } from './tasks/refresh.tokens';
import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport-new/bull.mq.module';
import { ConfigurationTask } from './tasks/configuration';
@Module({
imports: [ExternalCommandModule, DatabaseModule, BullMqModule],
controllers: [],
providers: [CheckStars, RefreshTokens],
providers: [CheckStars, RefreshTokens, ConfigurationTask],
get exports() {
return [...this.imports, ...this.providers];
},

View File

@ -0,0 +1,30 @@
import { Command } from 'nestjs-command';
import { Injectable } from '@nestjs/common';
import { ConfigurationChecker } from '@gitroom/helpers/configuration/configuration.checker';
@Injectable()
export class ConfigurationTask {
@Command({
command: 'config:check',
describe: 'Checks your configuration (.env) file for issues.',
})
create() {
const checker = new ConfigurationChecker();
checker.readEnvFromProcess()
checker.check()
if (checker.hasIssues()) {
for (const issue of checker.getIssues()) {
console.warn("Configuration issue:", issue)
}
console.error("Configuration check complete, issues: ", checker.getIssuesCount())
} else {
console.log("Configuration check complete, no issues found.")
}
console.log("Press Ctrl+C to exit.");
return true
}
}

View File

@ -0,0 +1,116 @@
import { readFileSync, existsSync } from 'fs'
import * as dotenv from 'dotenv'
import { resolve } from 'path'
export class ConfigurationChecker {
cfg: dotenv.DotenvParseOutput
issues: string[] = []
readEnvFromFile () {
const envFile = resolve(__dirname, '../../../.env')
if (!existsSync(envFile)) {
console.error('Env file not found!: ', envFile)
return
}
const handle = readFileSync(envFile, 'utf-8')
this.cfg = dotenv.parse(handle)
}
readEnvFromProcess () {
this.cfg = process.env
}
check () {
this.checkDatabaseServers()
this.checkNonEmpty('JWT_SECRET')
this.checkIsValidUrl('MAIN_URL')
this.checkIsValidUrl('FRONTEND_URL')
this.checkIsValidUrl('NEXT_PUBLIC_BACKEND_URL')
this.checkIsValidUrl('BACKEND_INTERNAL_URL')
this.checkNonEmpty('RESEND_API_KEY', 'Needed to send user activation emails.')
this.checkNonEmpty('CLOUDFLARE_ACCOUNT_ID', 'Needed to setup providers.')
this.checkNonEmpty('CLOUDFLARE_ACCESS_KEY', 'Needed to setup providers.')
this.checkNonEmpty('CLOUDFLARE_SECRET_ACCESS_KEY', 'Needed to setup providers.')
this.checkNonEmpty('CLOUDFLARE_BUCKETNAME', 'Needed to setup providers.')
this.checkNonEmpty('CLOUDFLARE_BUCKET_URL', 'Needed to setup providers.')
this.checkNonEmpty('CLOUDFLARE_REGION', 'Needed to setup providers.')
}
checkNonEmpty (key: string, description?: string): boolean {
const v = this.get(key)
if (!description) {
description = ''
}
if (!v) {
this.issues.push(key + ' not set. ' + description)
return false
}
if (v.length === 0) {
this.issues.push(key + ' is empty.' + description)
return false
}
return true
}
get(key: string): string | undefined {
return this.cfg[key as keyof typeof this.cfg]
}
checkDatabaseServers () {
this.checkRedis()
this.checkIsValidUrl('DATABASE_URL')
}
checkRedis () {
if (!this.cfg.REDIS_URL) {
this.issues.push('REDIS_URL not set')
}
try {
const redisUrl = new URL(this.cfg.REDIS_URL)
if (redisUrl.protocol !== 'redis:') {
this.issues.push('REDIS_URL must start with redis://')
}
} catch (error) {
this.issues.push('REDIS_URL is not a valid URL')
}
}
checkIsValidUrl (key: string) {
if (!this.checkNonEmpty(key)) {
return
}
const urlString = this.get(key)
try {
new URL(urlString)
} catch (error) {
this.issues.push(key + ' is not a valid URL')
}
if (urlString.endsWith('/')) {
this.issues.push(key + ' should not end with /')
}
}
hasIssues() {
return this.issues.length > 0
}
getIssues() {
return this.issues
}
getIssuesCount() {
return this.issues.length
}
}