feat(apps): api to enable domain and expose

This commit is contained in:
Nicolas Meienberger 2022-09-01 19:23:29 +00:00 committed by Nicolas Meienberger
parent 015e168634
commit 714a0d3af9
6 changed files with 77 additions and 31 deletions

View file

@ -57,8 +57,9 @@ const createApp = async (props: IProps) => {
MockFiles[`${config.ROOT_FOLDER}/repos/repo-id/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
MockFiles[`${config.ROOT_FOLDER}/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
let appEntity = new App();
if (installed) {
await App.create({
appEntity = await App.create({
id: appInfo.id,
config: { TEST_FIELD: 'test' },
status,
@ -70,7 +71,7 @@ const createApp = async (props: IProps) => {
MockFiles[`${config.ROOT_FOLDER}/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
}
return { appInfo, MockFiles };
return { appInfo, MockFiles, appEntity };
};
export { createApp };

View file

@ -3,6 +3,7 @@ import fs from 'fs-extra';
import { DataSource } from 'typeorm';
import config from '../../../config';
import { setupConnection, teardownConnection } from '../../../test/connection';
import App from '../app.entity';
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo, runAppScript } from '../apps.helpers';
import { AppInfo } from '../apps.types';
import { createApp } from './apps.factory';
@ -127,16 +128,19 @@ describe('runAppScript', () => {
describe('generateEnvFile', () => {
let app1: AppInfo;
let appEntity1: App;
beforeEach(async () => {
const app1create = await createApp({ installed: true });
app1 = app1create.appInfo;
appEntity1 = app1create.appEntity;
// @ts-ignore
fs.__createMockFiles(app1create.MockFiles);
});
it('Should generate an env file', async () => {
const fakevalue = faker.random.alphaNumeric(10);
generateEnvFile(app1.id, { TEST_FIELD: fakevalue });
generateEnvFile(Object.assign(appEntity1, { config: { TEST_FIELD: fakevalue } }));
const envmap = await getEnvMap(app1.id);
@ -144,11 +148,11 @@ describe('generateEnvFile', () => {
});
it('Should automatically generate value for random field', async () => {
const { appInfo, MockFiles } = await createApp({ installed: true, randomField: true });
const { appEntity, appInfo, MockFiles } = await createApp({ installed: true, randomField: true });
// @ts-ignore
fs.__createMockFiles(MockFiles);
generateEnvFile(appInfo.id, { TEST_FIELD: 'test' });
generateEnvFile(appEntity);
const envmap = await getEnvMap(appInfo.id);
@ -157,7 +161,7 @@ describe('generateEnvFile', () => {
});
it('Should not re-generate random field if it already exists', async () => {
const { appInfo, MockFiles } = await createApp({ installed: true, randomField: true });
const { appEntity, appInfo, MockFiles } = await createApp({ installed: true, randomField: true });
// @ts-ignore
fs.__createMockFiles(MockFiles);
@ -165,7 +169,7 @@ describe('generateEnvFile', () => {
fs.writeFileSync(`${config.ROOT_FOLDER}/app-data/${appInfo.id}/app.env`, `RANDOM_FIELD=${randomField}`);
generateEnvFile(appInfo.id, { TEST_FIELD: 'test' });
generateEnvFile(appEntity);
const envmap = await getEnvMap(appInfo.id);
@ -174,7 +178,7 @@ describe('generateEnvFile', () => {
it('Should throw an error if required field is not provided', async () => {
try {
generateEnvFile(app1.id, {});
generateEnvFile(Object.assign(appEntity1, { config: { TEST_FIELD: undefined } }));
expect(true).toBe(false);
} catch (e: any) {
expect(e).toBeDefined();
@ -184,7 +188,7 @@ describe('generateEnvFile', () => {
it('Should throw an error if app does not exist', async () => {
try {
generateEnvFile('not-existing-app', { TEST_FIELD: 'test' });
generateEnvFile(Object.assign(appEntity1, { id: 'not-existing-app' }));
expect(true).toBe(false);
} catch (e: any) {
expect(e).toBeDefined();
@ -220,7 +224,7 @@ describe('getAppInfo', () => {
it('Should return app info', async () => {
const appInfo = await getAppInfo(app1.id);
expect(appInfo.id).toBe(app1.id);
expect(appInfo?.id).toBe(app1.id);
});
it('Should take config.json locally if app is installed', async () => {
@ -232,7 +236,7 @@ describe('getAppInfo', () => {
const app = await getAppInfo(appInfo.id);
expect(app.id).toEqual(appInfo.id);
expect(app?.id).toEqual(appInfo.id);
});
it('Should throw an error if app does not exist', async () => {

View file

@ -74,19 +74,19 @@ const getEntropy = (name: string, length: number) => {
return hash.digest('hex').substring(0, length);
};
export const generateEnvFile = (appName: string, form: Record<string, string>) => {
const configFile: AppInfo | null = readJsonFile(`/apps/${appName}/config.json`);
export const generateEnvFile = (app: App) => {
const configFile: AppInfo | null = readJsonFile(`/apps/${app.id}/config.json`);
if (!configFile) {
throw new Error(`App ${appName} not found`);
throw new Error(`App ${app.id} not found`);
}
const baseEnvFile = readFile('/.env').toString();
let envFile = `${baseEnvFile}\nAPP_PORT=${configFile.port}\n`;
const envMap = getEnvMap(appName);
const envMap = getEnvMap(app.id);
configFile.form_fields?.forEach((field) => {
const formValue = form[field.env_variable];
const formValue = app.config[field.env_variable];
const envVar = field.env_variable;
if (formValue) {
@ -105,7 +105,12 @@ export const generateEnvFile = (appName: string, form: Record<string, string>) =
}
});
writeFile(`/app-data/${appName}/app.env`, envFile);
if (app.exposed && app.domain) {
envFile += 'APP_EXPOSED=true\n';
envFile += `APP_DOMAIN=${app.domain}\n`;
}
writeFile(`/app-data/${app.id}/app.env`, envFile);
};
export const getAvailableApps = async (): Promise<string[]> => {
@ -126,7 +131,7 @@ export const getAvailableApps = async (): Promise<string[]> => {
return apps;
};
export const getAppInfo = (id: string): AppInfo => {
export const getAppInfo = (id: string): AppInfo | null => {
try {
const repoId = config.APPS_REPO_ID;
@ -143,7 +148,7 @@ export const getAppInfo = (id: string): AppInfo => {
}
}
throw new Error('No repository found');
return null;
} catch (e) {
throw new Error(`Error loading app ${id}`);
}

View file

@ -24,9 +24,9 @@ export default class AppsResolver {
@Authorized()
@Mutation(() => App)
async installApp(@Arg('input', () => AppInputType) input: AppInputType): Promise<App> {
const { id, form } = input;
const { id, form, exposed, domain } = input;
return AppsService.installApp(id, form);
return AppsService.installApp(id, form, exposed, domain);
}
@Authorized()

View file

@ -15,7 +15,7 @@ const startAllApps = async (): Promise<void> => {
// Regenerate env file
try {
ensureAppFolder(app.id);
generateEnvFile(app.id, app.config);
generateEnvFile(app);
checkEnvFile(app.id);
await App.update({ id: app.id }, { status: AppStatusEnum.STARTING });
@ -40,7 +40,7 @@ const startApp = async (appName: string): Promise<App> => {
ensureAppFolder(appName);
// Regenerate env file
generateEnvFile(appName, app.config);
generateEnvFile(app);
checkEnvFile(appName);
@ -59,12 +59,16 @@ const startApp = async (appName: string): Promise<App> => {
return app;
};
const installApp = async (id: string, form: Record<string, string>): Promise<App> => {
const installApp = async (id: string, form: Record<string, string>, exposed?: boolean, domain?: string): Promise<App> => {
let app = await App.findOne({ where: { id } });
if (app) {
await startApp(id);
} else {
if (exposed && !domain) {
throw new Error('Domain is required if app is exposed');
}
ensureAppFolder(id, true);
const appIsValid = await checkAppRequirements(id);
@ -75,11 +79,16 @@ const installApp = async (id: string, form: Record<string, string>): Promise<App
// Create app folder
createFolder(`/app-data/${id}`);
// Create env file
generateEnvFile(id, form);
const appInfo: AppInfo | null = await readJsonFile(`/apps/${id}/config.json`);
app = await App.create({ id, status: AppStatusEnum.INSTALLING, config: form, version: Number(appInfo?.tipi_version || 0) }).save();
if (!appInfo?.exposeable && exposed) {
throw new Error(`App ${id} is not exposeable`);
}
app = await App.create({ id, status: AppStatusEnum.INSTALLING, config: form, version: Number(appInfo?.tipi_version || 0), exposed: exposed || false, domain }).save();
// Create env file
generateEnvFile(app);
// Run script
try {
@ -116,17 +125,35 @@ const listApps = async (): Promise<ListAppsResonse> => {
return { apps: apps.sort(sortApps), total: apps.length };
};
const updateAppConfig = async (id: string, form: Record<string, string>): Promise<App> => {
const updateAppConfig = async (id: string, form: Record<string, string>, exposed?: boolean, domain?: string): Promise<App> => {
if (exposed && !domain) {
throw new Error('Domain is required if app is exposed');
}
let app = await App.findOne({ where: { id } });
if (!app) {
throw new Error(`App ${id} not found`);
}
generateEnvFile(id, form);
await App.update({ id }, { config: form });
await App.update({ id }, { config: form, exposed: exposed || false, domain });
app = (await App.findOne({ where: { id } })) as App;
generateEnvFile(app);
app = (await App.findOne({ where: { id } })) as App;
// Restart app
try {
await App.update({ id }, { status: AppStatusEnum.STOPPING });
await runAppScript(['stop', id]);
await App.update({ id }, { status: AppStatusEnum.STARTING });
await runAppScript(['start', id]);
await App.update({ id }, { status: AppStatusEnum.RUNNING });
} catch (e) {
await App.update({ id }, { status: AppStatusEnum.STOPPED });
throw e;
}
return app;
};

View file

@ -125,6 +125,9 @@ class AppInfo {
@Field(() => Boolean, { nullable: true })
https?: boolean;
@Field(() => Boolean, { nullable: true })
exposeable?: boolean;
}
@ObjectType()
@ -143,6 +146,12 @@ class AppInputType {
@Field(() => GraphQLJSONObject)
form!: Record<string, string>;
@Field(() => Boolean, { nullable: true })
exposed?: boolean;
@Field(() => String, { nullable: true })
domain?: string;
}
export { ListAppsResonse, AppInfo, AppInputType };