Browse Source

feat(apps): api to enable domain and expose

Nicolas Meienberger 2 năm trước cách đây
mục cha
commit
714a0d3af9

+ 3 - 2
packages/system-api/src/modules/apps/__tests__/apps.factory.ts

@@ -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 };

+ 13 - 9
packages/system-api/src/modules/apps/__tests__/apps.helpers.test.ts

@@ -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 () => {

+ 13 - 8
packages/system-api/src/modules/apps/apps.helpers.ts

@@ -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}`);
   }

+ 2 - 2
packages/system-api/src/modules/apps/apps.resolver.ts

@@ -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()

+ 37 - 10
packages/system-api/src/modules/apps/apps.service.ts

@@ -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;
 };
 

+ 9 - 0
packages/system-api/src/modules/apps/apps.types.ts

@@ -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 };