feat: Configuration checking on backend startup, and via cli.
This commit is contained in:
parent
11de46f5b3
commit
d13c71d1f6
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue