feat: add demo mode option to start script

This commit is contained in:
Nicolas Meienberger 2023-03-30 12:36:58 +02:00
parent d5210a78a1
commit 018db408da
7 changed files with 168 additions and 55 deletions

View file

@ -31,6 +31,7 @@ POSTGRES_HOST=tipi-db
REDIS_HOST=tipi-redis
TIPI_VERSION=$(get_json_field "${ROOT_FOLDER}/package.json" version)
INTERNAL_IP=localhost
DEMO_MODE=false
storage_path="${ROOT_FOLDER}"
STORAGE_PATH_ESCAPED="$(echo "${storage_path}" | sed 's/\//\\\//g')"
if [[ "$ARCHITECTURE" == "aarch64" ]]; then
@ -157,6 +158,7 @@ for template in ${ENV_FILE}; do
sed "${sed_args[@]}" "s/<postgres_port>/${POSTGRES_PORT}/g" "${template}"
sed "${sed_args[@]}" "s/<postgres_host>/${POSTGRES_HOST}/g" "${template}"
sed "${sed_args[@]}" "s/<redis_host>/${REDIS_HOST}/g" "${template}"
sed "${sed_args[@]}" "s/<demo_mode>/${DEMO_MODE}/g" "${template}"
done
mv -f "$ENV_FILE" "$ROOT_FOLDER/.env.dev"

View file

@ -57,6 +57,7 @@ TIPI_VERSION=$(get_json_field "${ROOT_FOLDER}/package.json" version)
storage_path="${ROOT_FOLDER}"
STORAGE_PATH_ESCAPED="$(echo "${storage_path}" | sed 's/\//\\\//g')"
REDIS_HOST=tipi-redis
DEMO_MODE=false
INTERNAL_IP=
if [[ "$ARCHITECTURE" == "aarch64" ]] || [[ "$ARCHITECTURE" == "armv8"* ]]; then
@ -78,6 +79,7 @@ while [ -n "${1-}" ]; do
case "$1" in
--rc) rc="true" ;;
--ci) ci="true" ;;
--demo) DEMO_MODE=true ;;
--port)
port="${2-}"
@ -269,6 +271,7 @@ for template in ${ENV_FILE}; do
sed -i "s/<domain>/${DOMAIN}/g" "${template}"
sed -i "s/<storage_path>/${STORAGE_PATH_ESCAPED}/g" "${template}"
sed -i "s/<redis_host>/${REDIS_HOST}/g" "${template}"
sed -i "s/<demo_mode>/${DEMO_MODE}/g" "${template}"
done
mv -f "$ENV_FILE" "$ROOT_FOLDER/.env"

View file

@ -1,57 +1,87 @@
import { faker } from '@faker-js/faker';
import fs from 'fs-extra';
import { getConfig, setConfig, TipiConfig } from '.';
import { getConfig, setConfig, getSettings, setSettings, TipiConfig } from '.';
import { readJsonFile } from '../../common/fs.helpers';
beforeEach(async () => {
jest.resetModules();
jest.resetAllMocks();
// @ts-expect-error - We are mocking fs
fs.__resetAllMocks();
jest.mock('fs-extra');
});
describe('Test: getConfig', () => {
it('It should return config from .env', () => {
const config = getConfig();
jest.mock('next/config', () =>
jest.fn(() => ({
serverRuntimeConfig: {
DNS_IP: '1.1.1.1',
},
})),
);
// eslint-disable-next-line
import nextConfig from 'next/config';
describe('Test: process.env', () => {
it('should return config from .env', () => {
const config = new TipiConfig().getConfig();
expect(config).toBeDefined();
expect(config.dnsIp).toBe('1.1.1.1');
});
it('should throw an error if there are invalid values', () => {
// @ts-expect-error - We are mocking next/config
nextConfig.mockImplementationOnce(() => ({
serverRuntimeConfig: {
DNS_IP: 'invalid',
},
}));
expect(() => new TipiConfig().getConfig()).toThrow();
});
});
describe('Test: getConfig', () => {
it('It should return config from .env', () => {
// arrange
const config = getConfig();
// assert
expect(config).toBeDefined();
expect(config.NODE_ENV).toBe('test');
expect(config.dnsIp).toBe('9.9.9.9');
expect(config.rootFolder).toBe('/runtipi');
expect(config.internalIp).toBe('localhost');
});
it('It should overrides config from settings.json file', () => {
// arrange
const settingsJson = {
appsRepoUrl: faker.internet.url(),
appsRepoId: faker.random.word(),
domain: faker.random.word(),
};
const MockFiles = {
'/runtipi/state/settings.json': JSON.stringify(settingsJson),
};
// @ts-expect-error - We are mocking fs
fs.__createMockFiles(MockFiles);
// act
const config = new TipiConfig().getConfig();
// assert
expect(config).toBeDefined();
expect(config.appsRepoUrl).toBe(settingsJson.appsRepoUrl);
expect(config.appsRepoId).toBe(settingsJson.appsRepoId);
expect(config.domain).toBe(settingsJson.domain);
});
it('Should not be able to apply an invalid value from json config', () => {
// arrange
const settingsJson = {
appsRepoUrl: faker.random.word(),
appsRepoId: faker.random.word(),
domain: 10,
};
const MockFiles = {
'/runtipi/state/settings.json': JSON.stringify(settingsJson),
};
@ -59,16 +89,21 @@ describe('Test: getConfig', () => {
// @ts-expect-error - We are mocking fs
fs.__createMockFiles(MockFiles);
// act & assert
expect(() => new TipiConfig().getConfig()).toThrow();
});
});
describe('Test: setConfig', () => {
it('It should be able set config', () => {
// arrange
const randomWord = faker.internet.url();
// act
setConfig('appsRepoUrl', randomWord);
const config = getConfig();
// assert
expect(config).toBeDefined();
expect(config.appsRepoUrl).toBe(randomWord);
});
@ -115,7 +150,7 @@ describe('Test: getSettings', () => {
fs.__createMockFiles(MockFiles);
// act
const settings = new TipiConfig().getSettings();
const settings = getSettings();
// assert
expect(settings).toBeDefined();
@ -147,11 +182,10 @@ describe('Test: setSettings', () => {
};
// act
new TipiConfig().setSettings(fakeSettings);
// assert
setSettings(fakeSettings);
const settingsJson = readJsonFile('/runtipi/state/settings.json') as { [key: string]: string };
// assert
expect(settingsJson).toBeDefined();
expect(settingsJson.appsRepoUrl).toBe(fakeSettings.appsRepoUrl);
});
@ -161,12 +195,77 @@ describe('Test: setSettings', () => {
const fakeSettings = { appsRepoUrl: 10 };
// act
new TipiConfig().setSettings(fakeSettings as object);
// assert
setSettings(fakeSettings as object);
const settingsJson = (readJsonFile('/runtipi/state/settings.json') || {}) as { [key: string]: string };
// assert
expect(settingsJson).toBeDefined();
expect(settingsJson.appsRepoUrl).not.toBe(fakeSettings.appsRepoUrl);
});
it('should throw and error if demo mode is enabled', async () => {
// arrange
let error;
const fakeSettings = { appsRepoUrl: faker.internet.url() };
const tipiConf = new TipiConfig();
tipiConf.setConfig('demoMode', true);
// act
try {
await tipiConf.setSettings(fakeSettings);
} catch (e) {
error = e;
}
// assert
expect(error).toBeDefined();
});
it('should replace empty string with undefined if storagePath is empty', async () => {
// arrange
const fakeSettings = { storagePath: '' };
const tipiConf = new TipiConfig();
// act
await tipiConf.setSettings(fakeSettings);
// assert
expect(tipiConf.getConfig().storagePath).toBeUndefined();
});
it('should trim storagePath if it is not empty', async () => {
// arrange
const fakeSettings = { storagePath: ' /tmp ' };
const tipiConf = new TipiConfig();
// act
await tipiConf.setSettings(fakeSettings);
// assert
expect(tipiConf.getConfig().storagePath).toBe('/tmp');
});
it('should trim storagePath and return undefined if it is empty', async () => {
// arrange
const fakeSettings = { storagePath: ' ' };
const tipiConf = new TipiConfig();
// act
await tipiConf.setSettings(fakeSettings);
// assert
expect(tipiConf.getConfig().storagePath).toBeUndefined();
});
it('should remove all whitespaces from storagePath', async () => {
// arrange
const fakeSettings = { storagePath: ' /tmp /test ' };
const tipiConf = new TipiConfig();
// act
await tipiConf.setSettings(fakeSettings);
// assert
expect(tipiConf.getConfig().storagePath).toBe('/tmp/test');
});
});

View file

@ -11,26 +11,7 @@ export const ARCHITECTURES = {
} as const;
export type Architecture = (typeof ARCHITECTURES)[keyof typeof ARCHITECTURES];
const conf = { ...process.env, ...nextConfig()?.serverRuntimeConfig };
const {
NODE_ENV,
JWT_SECRET,
INTERNAL_IP,
TIPI_VERSION,
APPS_REPO_ID,
APPS_REPO_URL,
DOMAIN,
REDIS_HOST,
STORAGE_PATH,
ARCHITECTURE = 'amd64',
POSTGRES_HOST,
POSTGRES_DBNAME,
POSTGRES_USERNAME,
POSTGRES_PASSWORD,
POSTGRES_PORT = 5432,
} = conf;
export const configSchema = z.object({
const configSchema = z.object({
NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]),
REDIS_HOST: z.string(),
status: z.union([z.literal('RUNNING'), z.literal('UPDATING'), z.literal('RESTARTING')]),
@ -56,12 +37,21 @@ export const configSchema = z.object({
postgresUsername: z.string(),
postgresPassword: z.string(),
postgresPort: z.number(),
demoMode: z
.string()
.or(z.boolean())
.optional()
.transform((value) => {
if (typeof value === 'boolean') return value;
return value === 'true';
}),
});
export const settingsSchema = configSchema.partial().pick({ dnsIp: true, internalIp: true, appsRepoUrl: true, domain: true, storagePath: true });
export type TipiSettingsType = z.infer<typeof settingsSchema>;
export const formatErrors = (errors: { fieldErrors: Record<string, string[]> }) =>
const formatErrors = (errors: { fieldErrors: Record<string, string[]> }) =>
Object.entries(errors.fieldErrors)
.map(([name, value]) => `${name}: ${value[0]}`)
.filter(Boolean)
@ -73,25 +63,27 @@ export class TipiConfig {
private config: z.infer<typeof configSchema>;
constructor() {
const conf = { ...process.env, ...nextConfig()?.serverRuntimeConfig };
const envConfig: z.infer<typeof configSchema> = {
postgresHost: POSTGRES_HOST,
postgresDatabase: POSTGRES_DBNAME,
postgresUsername: POSTGRES_USERNAME,
postgresPassword: POSTGRES_PASSWORD,
postgresPort: Number(POSTGRES_PORT),
REDIS_HOST,
NODE_ENV,
architecture: ARCHITECTURE as z.infer<typeof configSchema>['architecture'],
postgresHost: conf.POSTGRES_HOST,
postgresDatabase: conf.POSTGRES_DBNAME,
postgresUsername: conf.POSTGRES_USERNAME,
postgresPassword: conf.POSTGRES_PASSWORD,
postgresPort: Number(conf.POSTGRES_PORT || 5432),
REDIS_HOST: conf.REDIS_HOST,
NODE_ENV: conf.NODE_ENV,
architecture: conf.ARCHITECTURE || 'amd64',
rootFolder: '/runtipi',
internalIp: INTERNAL_IP,
version: TIPI_VERSION,
jwtSecret: JWT_SECRET,
appsRepoId: APPS_REPO_ID,
appsRepoUrl: APPS_REPO_URL,
domain: DOMAIN,
dnsIp: '9.9.9.9',
internalIp: conf.INTERNAL_IP,
version: conf.TIPI_VERSION,
jwtSecret: conf.JWT_SECRET,
appsRepoId: conf.APPS_REPO_ID,
appsRepoUrl: conf.APPS_REPO_URL,
domain: conf.DOMAIN,
dnsIp: conf.DNS_IP || '9.9.9.9',
status: 'RUNNING',
storagePath: STORAGE_PATH,
storagePath: conf.STORAGE_PATH,
demoMode: conf.DEMO_MODE,
};
const fileConfig = readJsonFile('/runtipi/state/settings.json') || {};
@ -156,6 +148,10 @@ export class TipiConfig {
}
public async setSettings(settings: TipiSettingsType) {
if (this.config.demoMode) {
throw new Error('Cannot update settings in demo mode');
}
const newConf: z.infer<typeof configSchema> = { ...this.getConfig() };
const parsed = settingsSchema.safeParse(settings);

View file

@ -122,6 +122,14 @@ describe('Test: restart', () => {
// Assert
expect(restart).toBeTruthy();
});
it('should throw an error in demo mode', async () => {
// Arrange
await setConfig('demoMode', true);
// Act & Assert
await expect(SystemService.restart()).rejects.toThrow('Cannot restart in demo mode');
});
});
describe('Test: update', () => {

View file

@ -105,6 +105,10 @@ export class SystemServiceClass {
throw new Error('Cannot restart in development mode');
}
if (TipiConfig.getConfig().demoMode) {
throw new Error('Cannot restart in demo mode');
}
TipiConfig.setConfig('status', 'RESTARTING');
this.dispatcher.dispatchEventAsync('restart');

View file

@ -20,3 +20,4 @@ POSTGRES_USERNAME=<postgres_username>
POSTGRES_PASSWORD=<postgres_password>
POSTGRES_PORT=<postgres_port>
REDIS_HOST=<redis_host>
DEMO_MODE=<demo_mode>