Skip to content

Commit

Permalink
Merge pull request #203 from jamesread/configuration-checking
Browse files Browse the repository at this point in the history
feat: Configuration checking on backend startup, and via cli.
  • Loading branch information
jamesread authored Sep 25, 2024
2 parents 5fcc365 + c766d53 commit d25a33e
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 1 deletion.
19 changes: 19 additions & 0 deletions apps/backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -38,11 +39,29 @@ async function bootstrap() {

try {
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(`🚀 Backend is running on: http://localhost:${port}`);
} catch (e) {
Logger.error(`Backend failed to start on port ${port}`, e);
}
}

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())
} else {
Logger.log("Configuration check completed without any issues.")
}
}

bootstrap();
3 changes: 2 additions & 1 deletion apps/commands/src/command.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
},
Expand Down
30 changes: 30 additions & 0 deletions apps/commands/src/tasks/configuration.ts
Original file line number Diff line number Diff line change
@@ -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
}
}

116 changes: 116 additions & 0 deletions libraries/helpers/src/configuration/configuration.checker.ts
Original file line number Diff line number Diff line change
@@ -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
}
}

0 comments on commit d25a33e

Please sign in to comment.