Browse Source

feat: start apps on launch

Nicolas Meienberger 1 year ago
parent
commit
25d21a3bc1

+ 59 - 0
packages/cli/src/executors/app/app.executors.ts

@@ -1,15 +1,35 @@
+/* eslint-disable no-await-in-loop */
+/* eslint-disable no-restricted-syntax */
 import fs from 'fs';
 import fs from 'fs';
 import path from 'path';
 import path from 'path';
 import { exec } from 'child_process';
 import { exec } from 'child_process';
 import { promisify } from 'util';
 import { promisify } from 'util';
+import pg from 'pg';
 import { getEnv } from '@/utils/environment/environment';
 import { getEnv } from '@/utils/environment/environment';
 import { pathExists } from '@/utils/fs-helpers';
 import { pathExists } from '@/utils/fs-helpers';
 import { compose } from '@/utils/docker-helpers';
 import { compose } from '@/utils/docker-helpers';
 import { copyDataDir, generateEnvFile } from './app.helpers';
 import { copyDataDir, generateEnvFile } from './app.helpers';
 import { fileLogger } from '@/utils/logger/file-logger';
 import { fileLogger } from '@/utils/logger/file-logger';
+import { TerminalSpinner } from '@/utils/logger/terminal-spinner';
 
 
 const execAsync = promisify(exec);
 const execAsync = promisify(exec);
 
 
+const getDbClient = async () => {
+  const { postgresDatabase, postgresUsername, postgresPassword, postgresPort } = getEnv();
+
+  const client = new pg.Client({
+    host: '127.0.0.1',
+    database: postgresDatabase,
+    user: postgresUsername,
+    password: postgresPassword,
+    port: Number(postgresPort),
+  });
+
+  await client.connect();
+
+  return client;
+};
+
 export class AppExecutors {
 export class AppExecutors {
   private readonly logger;
   private readonly logger;
 
 
@@ -235,4 +255,43 @@ export class AppExecutors {
       return this.handleAppError(err);
       return this.handleAppError(err);
     }
     }
   };
   };
+
+  /**
+   * Start all apps with status running
+   */
+  public startAllApps = async () => {
+    const spinner = new TerminalSpinner('Starting apps...');
+    const client = await getDbClient();
+
+    try {
+      // Get all apps with status running
+      const { rows } = await client.query(`SELECT * FROM app WHERE status = 'running'`);
+
+      // Update all apps with status different than running or stopped to stopped
+      await client.query(`UPDATE app SET status = 'stopped' WHERE status != 'stopped' AND status != 'running' AND status != 'missing'`);
+
+      // Start all apps
+      for (const row of rows) {
+        spinner.setMessage(`Starting app ${row.id}`);
+        spinner.start();
+        const { id, config } = row;
+
+        const { success } = await this.startApp(id, config);
+
+        if (!success) {
+          this.logger.error(`Error starting app ${id}`);
+          await client.query(`UPDATE app SET status = 'stopped' WHERE id = '${id}'`);
+          spinner.fail(`Error starting app ${id}`);
+        } else {
+          await client.query(`UPDATE app SET status = 'running' WHERE id = '${id}'`);
+          spinner.done(`App ${id} started`);
+        }
+      }
+    } catch (err) {
+      this.logger.error(`Error starting apps: ${err}`);
+      spinner.fail(`Error starting apps see logs for details (logs/error.log)`);
+    } finally {
+      await client.end();
+    }
+  };
 }
 }

+ 49 - 24
packages/cli/src/executors/system/system.executors.ts

@@ -58,16 +58,8 @@ export class SystemExecutors {
     };
     };
   };
   };
 
 
-  private ensureFilePermissions = async (rootFolderHost: string, logSudoRequest = true) => {
+  private ensureFilePermissions = async (rootFolderHost: string) => {
     const logger = new TerminalSpinner('');
     const logger = new TerminalSpinner('');
-    // if we are running as root, we don't need to change permissions
-    if (process.getuid && process.getuid() === 0) {
-      return;
-    }
-
-    if (logSudoRequest) {
-      logger.log('Tipi needs to change permissions on some files and folders and will ask for your password.');
-    }
 
 
     // Create group tipi if it does not exist
     // Create group tipi if it does not exist
     try {
     try {
@@ -106,11 +98,25 @@ export class SystemExecutors {
       path.join(rootFolderHost, 'VERSION'),
       path.join(rootFolderHost, 'VERSION'),
     ];
     ];
 
 
+    const files600 = [path.join(rootFolderHost, 'traefik', 'acme.json')];
+
     // Give permission to read and write to all files and folders for the current user
     // Give permission to read and write to all files and folders for the current user
     await Promise.all(
     await Promise.all(
       filesAndFolders.map(async (fileOrFolder) => {
       filesAndFolders.map(async (fileOrFolder) => {
         if (await pathExists(fileOrFolder)) {
         if (await pathExists(fileOrFolder)) {
-          await execAsync(`sudo chmod -R a+rwx ${fileOrFolder}`);
+          await execAsync(`sudo chmod -R a+rwx ${fileOrFolder}`).catch(() => {
+            logger.fail(`Failed to set permissions on ${fileOrFolder}`);
+          });
+        }
+      }),
+    );
+
+    await Promise.all(
+      files600.map(async (fileOrFolder) => {
+        if (await pathExists(fileOrFolder)) {
+          await execAsync(`sudo chmod 600 ${fileOrFolder}`).catch(() => {
+            logger.fail(`Failed to set permissions on ${fileOrFolder}`);
+          });
         }
         }
       }),
       }),
     );
     );
@@ -149,15 +155,6 @@ export class SystemExecutors {
           await appExecutor.stopApp(app, {}, true);
           await appExecutor.stopApp(app, {}, true);
           spinner.done(`${app} stopped`);
           spinner.done(`${app} stopped`);
         }
         }
-
-        await Promise.all(
-          apps.map(async (app) => {
-            const appSpinner = new TerminalSpinner(`Stopping ${app}...`);
-            appSpinner.start();
-            await appExecutor.stopApp(app, {}, true);
-            appSpinner.done(`${app} stopped`);
-          }),
-        );
       }
       }
 
 
       spinner.setMessage('Stopping containers...');
       spinner.setMessage('Stopping containers...');
@@ -177,17 +174,37 @@ export class SystemExecutors {
    * This method will start Tipi.
    * This method will start Tipi.
    * It will copy the system files, generate the system env file, pull the images and start the containers.
    * It will copy the system files, generate the system env file, pull the images and start the containers.
    */
    */
-  public start = async () => {
+  public start = async (sudo = true) => {
     const spinner = new TerminalSpinner('Starting Tipi...');
     const spinner = new TerminalSpinner('Starting Tipi...');
     try {
     try {
       const { isSudo } = getUserIds();
       const { isSudo } = getUserIds();
 
 
-      if (!isSudo) {
+      if (!sudo) {
+        console.log(
+          boxen(
+            "You are running in sudoless mode. While Tipi should work as expected, you'll probably run into permission issues and will have to manually fix them. We recommend running Tipi with sudo for beginners.",
+            {
+              title: '⛔️Sudoless mode',
+              titleAlignment: 'center',
+              textAlignment: 'center',
+              padding: 1,
+              borderStyle: 'double',
+              borderColor: 'red',
+              margin: { top: 1, bottom: 1 },
+              width: 80,
+            },
+          ),
+        );
+      }
+
+      if (!isSudo && sudo) {
         console.log(chalk.red('Tipi needs to run as root to start. Use sudo ./runtipi-cli start'));
         console.log(chalk.red('Tipi needs to run as root to start. Use sudo ./runtipi-cli start'));
         throw new Error('Tipi needs to run as root to start. Use sudo ./runtipi-cli start');
         throw new Error('Tipi needs to run as root to start. Use sudo ./runtipi-cli start');
       }
       }
 
 
-      await this.ensureFilePermissions(this.rootFolder);
+      if (sudo) {
+        await this.ensureFilePermissions(this.rootFolder);
+      }
 
 
       spinner.start();
       spinner.start();
       spinner.setMessage('Copying system files...');
       spinner.setMessage('Copying system files...');
@@ -195,7 +212,9 @@ export class SystemExecutors {
 
 
       spinner.done('System files copied');
       spinner.done('System files copied');
 
 
-      await this.ensureFilePermissions(this.rootFolder, false);
+      if (sudo) {
+        await this.ensureFilePermissions(this.rootFolder);
+      }
 
 
       spinner.setMessage('Generating system env file...');
       spinner.setMessage('Generating system env file...');
       spinner.start();
       spinner.start();
@@ -270,13 +289,19 @@ export class SystemExecutors {
 
 
       spinner.done('Database migrations complete');
       spinner.done('Database migrations complete');
 
 
+      // Start all apps
+      const appExecutor = new AppExecutors();
+      await appExecutor.startAllApps();
+
       console.log(
       console.log(
         boxen(`Visit: http://${envMap.get('INTERNAL_IP')}:${envMap.get('NGINX_PORT')} to access the dashboard\n\nFind documentation and guides at: https://runtipi.io`, {
         boxen(`Visit: http://${envMap.get('INTERNAL_IP')}:${envMap.get('NGINX_PORT')} to access the dashboard\n\nFind documentation and guides at: https://runtipi.io`, {
           title: 'Tipi successfully started 🎉',
           title: 'Tipi successfully started 🎉',
           titleAlignment: 'center',
           titleAlignment: 'center',
+          textAlignment: 'center',
           padding: 1,
           padding: 1,
           borderStyle: 'double',
           borderStyle: 'double',
           borderColor: 'green',
           borderColor: 'green',
+          width: 80,
           margin: { top: 1 },
           margin: { top: 1 },
         }),
         }),
       );
       );
@@ -327,7 +352,7 @@ export class SystemExecutors {
 
 
       if (!targetVersion || targetVersion === 'latest') {
       if (!targetVersion || targetVersion === 'latest') {
         spinner.setMessage('Fetching latest version...');
         spinner.setMessage('Fetching latest version...');
-        const { data } = await axios.get<{ tag_name: string }>('https://api.github.com/repos/meienberger/runtipi/releases');
+        const { data } = await axios.get<{ tag_name: string }>('https://api.github.com/repos/meienberger/runtipi/releases/latest');
         targetVersion = data.tag_name;
         targetVersion = data.tag_name;
       }
       }
 
 

+ 2 - 2
packages/cli/src/index.ts

@@ -22,9 +22,9 @@ const main = async () => {
     .description('Start tipi')
     .description('Start tipi')
     .option('--no-permissions', 'Skip permissions check')
     .option('--no-permissions', 'Skip permissions check')
     .option('--no-sudo', 'Skip sudo usage')
     .option('--no-sudo', 'Skip sudo usage')
-    .action(async () => {
+    .action(async (options) => {
       const systemExecutors = new SystemExecutors();
       const systemExecutors = new SystemExecutors();
-      await systemExecutors.start();
+      await systemExecutors.start(options.sudo);
     });
     });
 
 
   program
   program

+ 10 - 1
packages/cli/src/utils/environment/environment.ts

@@ -16,9 +16,14 @@ const environmentSchema = z
     INTERNAL_IP: z.string().ip().or(z.literal('localhost')),
     INTERNAL_IP: z.string().ip().or(z.literal('localhost')),
     TIPI_VERSION: z.string(),
     TIPI_VERSION: z.string(),
     REDIS_PASSWORD: z.string(),
     REDIS_PASSWORD: z.string(),
+    POSTGRES_PORT: z.string(),
+    POSTGRES_USERNAME: z.string(),
+    POSTGRES_PASSWORD: z.string(),
+    POSTGRES_DBNAME: z.string(),
   })
   })
   .transform((env) => {
   .transform((env) => {
-    const { STORAGE_PATH, ARCHITECTURE, ROOT_FOLDER_HOST, APPS_REPO_ID, INTERNAL_IP, TIPI_VERSION, REDIS_PASSWORD, ...rest } = env;
+    const { STORAGE_PATH, ARCHITECTURE, ROOT_FOLDER_HOST, APPS_REPO_ID, INTERNAL_IP, TIPI_VERSION, REDIS_PASSWORD, POSTGRES_DBNAME, POSTGRES_PASSWORD, POSTGRES_USERNAME, POSTGRES_PORT, ...rest } =
+      env;
 
 
     return {
     return {
       storagePath: STORAGE_PATH,
       storagePath: STORAGE_PATH,
@@ -28,6 +33,10 @@ const environmentSchema = z
       tipiVersion: TIPI_VERSION,
       tipiVersion: TIPI_VERSION,
       internalIp: INTERNAL_IP,
       internalIp: INTERNAL_IP,
       redisPassword: REDIS_PASSWORD,
       redisPassword: REDIS_PASSWORD,
+      postgresPort: POSTGRES_PORT,
+      postgresUsername: POSTGRES_USERNAME,
+      postgresPassword: POSTGRES_PASSWORD,
+      postgresDatabase: POSTGRES_DBNAME,
       ...rest,
       ...rest,
     };
     };
   });
   });