feat(apps): api to enable domain and expose
This commit is contained in:
parent
015e168634
commit
714a0d3af9
6 changed files with 77 additions and 31 deletions
|
@ -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 };
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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 };
|
||||
|
|
Loading…
Reference in a new issue