refactor(server): env validation (#13817)

This commit is contained in:
Jason Rasmussen
2024-10-30 05:00:41 -04:00
committed by GitHub
parent 19eb3ed8b9
commit 0f668fd5c6
8 changed files with 305 additions and 149 deletions

View File

@@ -8,6 +8,7 @@ const getEnv = () => {
const resetEnv = () => {
for (const env of [
'IMMICH_ENV',
'IMMICH_WORKERS_INCLUDE',
'IMMICH_WORKERS_EXCLUDE',
'IMMICH_TRUSTED_PROXIES',
@@ -62,6 +63,18 @@ describe('getEnv', () => {
resetEnv();
});
it('should use defaults', () => {
const config = getEnv();
expect(config).toMatchObject({
host: undefined,
port: 2283,
environment: 'production',
configFile: undefined,
logLevel: undefined,
});
});
describe('database', () => {
it('should use defaults', () => {
const { database } = getEnv();
@@ -202,6 +215,11 @@ describe('getEnv', () => {
trustedProxies: ['10.1.0.0', '10.2.0.0', '169.254.0.0/16'],
});
});
it('should reject invalid trusted proxies', () => {
process.env.IMMICH_TRUSTED_PROXIES = '10.1';
expect(() => getEnv()).toThrowError('Invalid environment variables: IMMICH_TRUSTED_PROXIES');
});
});
describe('telemetry', () => {

View File

@@ -1,17 +1,18 @@
import { Injectable } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { validateSync } from 'class-validator';
import { Request, Response } from 'express';
import { CLS_ID } from 'nestjs-cls';
import { join, resolve } from 'node:path';
import { citiesFile, excludePaths } from 'src/constants';
import { Telemetry } from 'src/decorators';
import { ImmichEnvironment, ImmichHeader, ImmichTelemetry, ImmichWorker, LogLevel } from 'src/enum';
import { EnvDto } from 'src/dtos/env.dto';
import { ImmichEnvironment, ImmichHeader, ImmichTelemetry, ImmichWorker } from 'src/enum';
import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
import { DatabaseExtension } from 'src/interfaces/database.interface';
import { QueueName } from 'src/interfaces/job.interface';
import { setDifference } from 'src/utils/set';
// TODO replace src/config validation with class-validator, here
const productionKeys = {
client:
'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF2LzdTMzJjUkE1KysxTm5WRHNDTQpzcFAvakpISU1xT0pYRm5oNE53QTJPcHorUk1mZGNvOTJQc09naCt3d1FlRXYxVTJjMnBqelRpUS8ybHJLcS9rCnpKUmxYd2M0Y1Vlc1FETUpPRitQMnFPTlBiQUprWHZDWFlCVUxpdENJa29Md2ZoU0dOanlJS2FSRGhkL3ROeU4KOCtoTlJabllUMWhTSWo5U0NrS3hVQ096YXRQVjRtQ0RlclMrYkUrZ0VVZVdwOTlWOWF6dkYwRkltblRXcFFTdwpjOHdFWmdPTWg0c3ZoNmFpY3dkemtQQ3dFTGFrMFZhQkgzMUJFVUNRTGI5K0FJdEhBVXRKQ0t4aGI1V2pzMXM5CmJyWGZpMHZycGdjWi82RGFuWTJxZlNQem5PbXZEMkZycmxTMXE0SkpOM1ZvN1d3LzBZeS95TWNtelRXWmhHdWgKVVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo=',
@@ -35,8 +36,16 @@ const asSet = <T>(value: string | undefined, defaults: T[]) => {
};
const getEnv = (): EnvData => {
const includedWorkers = asSet(process.env.IMMICH_WORKERS_INCLUDE, [ImmichWorker.API, ImmichWorker.MICROSERVICES]);
const excludedWorkers = asSet(process.env.IMMICH_WORKERS_EXCLUDE, []);
const dto = plainToInstance(EnvDto, process.env);
const errors = validateSync(dto);
if (errors.length > 0) {
throw new Error(
`Invalid environment variables: ${errors.map((error) => `${error.property}=${error.value}`).join(', ')}`,
);
}
const includedWorkers = asSet(dto.IMMICH_WORKERS_INCLUDE, [ImmichWorker.API, ImmichWorker.MICROSERVICES]);
const excludedWorkers = asSet(dto.IMMICH_WORKERS_EXCLUDE, []);
const workers = [...setDifference(includedWorkers, excludedWorkers)];
for (const worker of workers) {
if (!WORKER_TYPES.has(worker)) {
@@ -44,9 +53,9 @@ const getEnv = (): EnvData => {
}
}
const environment = process.env.IMMICH_ENV as ImmichEnvironment;
const environment = dto.IMMICH_ENV || ImmichEnvironment.PRODUCTION;
const isProd = environment === ImmichEnvironment.PRODUCTION;
const buildFolder = process.env.IMMICH_BUILD_DATA || '/build';
const buildFolder = dto.IMMICH_BUILD_DATA || '/build';
const folders = {
// eslint-disable-next-line unicorn/prefer-module
dist: resolve(`${__dirname}/..`),
@@ -54,18 +63,18 @@ const getEnv = (): EnvData => {
web: join(buildFolder, 'www'),
};
const databaseUrl = process.env.DB_URL;
const databaseUrl = dto.DB_URL;
let redisConfig = {
host: process.env.REDIS_HOSTNAME || 'redis',
port: Number.parseInt(process.env.REDIS_PORT || '') || 6379,
db: Number.parseInt(process.env.REDIS_DBINDEX || '') || 0,
username: process.env.REDIS_USERNAME || undefined,
password: process.env.REDIS_PASSWORD || undefined,
path: process.env.REDIS_SOCKET || undefined,
host: dto.REDIS_HOSTNAME || 'redis',
port: dto.REDIS_PORT || 6379,
db: dto.REDIS_DBINDEX || 0,
username: dto.REDIS_USERNAME || undefined,
password: dto.REDIS_PASSWORD || undefined,
path: dto.REDIS_SOCKET || undefined,
};
const redisUrl = process.env.REDIS_URL;
const redisUrl = dto.REDIS_URL;
if (redisUrl && redisUrl.startsWith('ioredis://')) {
try {
redisConfig = JSON.parse(Buffer.from(redisUrl.slice(10), 'base64').toString());
@@ -75,11 +84,11 @@ const getEnv = (): EnvData => {
}
const includedTelemetries =
process.env.IMMICH_TELEMETRY_INCLUDE === 'all'
dto.IMMICH_TELEMETRY_INCLUDE === 'all'
? new Set(Object.values(ImmichTelemetry))
: asSet<ImmichTelemetry>(process.env.IMMICH_TELEMETRY_INCLUDE, []);
: asSet<ImmichTelemetry>(dto.IMMICH_TELEMETRY_INCLUDE, []);
const excludedTelemetries = asSet<ImmichTelemetry>(process.env.IMMICH_TELEMETRY_EXCLUDE, []);
const excludedTelemetries = asSet<ImmichTelemetry>(dto.IMMICH_TELEMETRY_EXCLUDE, []);
const telemetries = setDifference(includedTelemetries, excludedTelemetries);
for (const telemetry of telemetries) {
if (!TELEMETRY_TYPES.has(telemetry)) {
@@ -88,26 +97,26 @@ const getEnv = (): EnvData => {
}
return {
host: process.env.IMMICH_HOST,
port: Number(process.env.IMMICH_PORT) || 2283,
host: dto.IMMICH_HOST,
port: dto.IMMICH_PORT || 2283,
environment,
configFile: process.env.IMMICH_CONFIG_FILE,
logLevel: process.env.IMMICH_LOG_LEVEL as LogLevel,
configFile: dto.IMMICH_CONFIG_FILE,
logLevel: dto.IMMICH_LOG_LEVEL,
buildMetadata: {
build: process.env.IMMICH_BUILD,
buildUrl: process.env.IMMICH_BUILD_URL,
buildImage: process.env.IMMICH_BUILD_IMAGE,
buildImageUrl: process.env.IMMICH_BUILD_IMAGE_URL,
repository: process.env.IMMICH_REPOSITORY,
repositoryUrl: process.env.IMMICH_REPOSITORY_URL,
sourceRef: process.env.IMMICH_SOURCE_REF,
sourceCommit: process.env.IMMICH_SOURCE_COMMIT,
sourceUrl: process.env.IMMICH_SOURCE_URL,
thirdPartySourceUrl: process.env.IMMICH_THIRD_PARTY_SOURCE_URL,
thirdPartyBugFeatureUrl: process.env.IMMICH_THIRD_PARTY_BUG_FEATURE_URL,
thirdPartyDocumentationUrl: process.env.IMMICH_THIRD_PARTY_DOCUMENTATION_URL,
thirdPartySupportUrl: process.env.IMMICH_THIRD_PARTY_SUPPORT_URL,
build: dto.IMMICH_BUILD,
buildUrl: dto.IMMICH_BUILD_URL,
buildImage: dto.IMMICH_BUILD_IMAGE,
buildImageUrl: dto.IMMICH_BUILD_IMAGE_URL,
repository: dto.IMMICH_REPOSITORY,
repositoryUrl: dto.IMMICH_REPOSITORY_URL,
sourceRef: dto.IMMICH_SOURCE_REF,
sourceCommit: dto.IMMICH_SOURCE_COMMIT,
sourceUrl: dto.IMMICH_SOURCE_URL,
thirdPartySourceUrl: dto.IMMICH_THIRD_PARTY_SOURCE_URL,
thirdPartyBugFeatureUrl: dto.IMMICH_THIRD_PARTY_BUG_FEATURE_URL,
thirdPartyDocumentationUrl: dto.IMMICH_THIRD_PARTY_DOCUMENTATION_URL,
thirdPartySupportUrl: dto.IMMICH_THIRD_PARTY_SUPPORT_URL,
},
bull: {
@@ -153,26 +162,22 @@ const getEnv = (): EnvData => {
? { connectionType: 'url', url: databaseUrl }
: {
connectionType: 'parts',
host: process.env.DB_HOSTNAME || 'database',
port: Number(process.env.DB_PORT) || 5432,
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
database: process.env.DB_DATABASE_NAME || 'immich',
host: dto.DB_HOSTNAME || 'database',
port: dto.DB_PORT || 5432,
username: dto.DB_USERNAME || 'postgres',
password: dto.DB_PASSWORD || 'postgres',
database: dto.DB_DATABASE_NAME || 'immich',
}),
},
skipMigrations: process.env.DB_SKIP_MIGRATIONS === 'true',
vectorExtension:
process.env.DB_VECTOR_EXTENSION === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS,
skipMigrations: dto.DB_SKIP_MIGRATIONS ?? false,
vectorExtension: dto.DB_VECTOR_EXTENSION === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS,
},
licensePublicKey: isProd ? productionKeys : stagingKeys,
network: {
trustedProxies: (process.env.IMMICH_TRUSTED_PROXIES ?? '')
.split(',')
.map((value) => value.trim())
.filter(Boolean),
trustedProxies: dto.IMMICH_TRUSTED_PROXIES ?? [],
},
otel: {
@@ -203,18 +208,18 @@ const getEnv = (): EnvData => {
},
storage: {
ignoreMountCheckErrors: process.env.IMMICH_IGNORE_MOUNT_CHECK_ERRORS === 'true',
ignoreMountCheckErrors: !!dto.IMMICH_IGNORE_MOUNT_CHECK_ERRORS,
},
telemetry: {
apiPort: Number(process.env.IMMICH_API_METRICS_PORT || '') || 8081,
microservicesPort: Number(process.env.IMMICH_MICROSERVICES_METRICS_PORT || '') || 8082,
apiPort: dto.IMMICH_API_METRICS_PORT || 8081,
microservicesPort: dto.IMMICH_MICROSERVICES_METRICS_PORT || 8082,
metrics: telemetries,
},
workers,
noColor: !!process.env.NO_COLOR,
noColor: !!dto.NO_COLOR,
};
};