feat: start apps on launch
This commit is contained in:
parent
e5f1a94c08
commit
25d21a3bc1
4 changed files with 120 additions and 27 deletions
|
@ -1,15 +1,35 @@
|
|||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import pg from 'pg';
|
||||
import { getEnv } from '@/utils/environment/environment';
|
||||
import { pathExists } from '@/utils/fs-helpers';
|
||||
import { compose } from '@/utils/docker-helpers';
|
||||
import { copyDataDir, generateEnvFile } from './app.helpers';
|
||||
import { fileLogger } from '@/utils/logger/file-logger';
|
||||
import { TerminalSpinner } from '@/utils/logger/terminal-spinner';
|
||||
|
||||
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 {
|
||||
private readonly logger;
|
||||
|
||||
|
@ -235,4 +255,43 @@ export class AppExecutors {
|
|||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -58,16 +58,8 @@ export class SystemExecutors {
|
|||
};
|
||||
};
|
||||
|
||||
private ensureFilePermissions = async (rootFolderHost: string, logSudoRequest = true) => {
|
||||
private ensureFilePermissions = async (rootFolderHost: string) => {
|
||||
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
|
||||
try {
|
||||
|
@ -106,11 +98,25 @@ export class SystemExecutors {
|
|||
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
|
||||
await Promise.all(
|
||||
filesAndFolders.map(async (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);
|
||||
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...');
|
||||
|
@ -177,17 +174,37 @@ export class SystemExecutors {
|
|||
* This method will start Tipi.
|
||||
* 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...');
|
||||
try {
|
||||
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'));
|
||||
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.setMessage('Copying system files...');
|
||||
|
@ -195,7 +212,9 @@ export class SystemExecutors {
|
|||
|
||||
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.start();
|
||||
|
@ -270,13 +289,19 @@ export class SystemExecutors {
|
|||
|
||||
spinner.done('Database migrations complete');
|
||||
|
||||
// Start all apps
|
||||
const appExecutor = new AppExecutors();
|
||||
await appExecutor.startAllApps();
|
||||
|
||||
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`, {
|
||||
title: 'Tipi successfully started 🎉',
|
||||
titleAlignment: 'center',
|
||||
textAlignment: 'center',
|
||||
padding: 1,
|
||||
borderStyle: 'double',
|
||||
borderColor: 'green',
|
||||
width: 80,
|
||||
margin: { top: 1 },
|
||||
}),
|
||||
);
|
||||
|
@ -327,7 +352,7 @@ export class SystemExecutors {
|
|||
|
||||
if (!targetVersion || targetVersion === 'latest') {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -22,9 +22,9 @@ const main = async () => {
|
|||
.description('Start tipi')
|
||||
.option('--no-permissions', 'Skip permissions check')
|
||||
.option('--no-sudo', 'Skip sudo usage')
|
||||
.action(async () => {
|
||||
.action(async (options) => {
|
||||
const systemExecutors = new SystemExecutors();
|
||||
await systemExecutors.start();
|
||||
await systemExecutors.start(options.sudo);
|
||||
});
|
||||
|
||||
program
|
||||
|
|
|
@ -16,9 +16,14 @@ const environmentSchema = z
|
|||
INTERNAL_IP: z.string().ip().or(z.literal('localhost')),
|
||||
TIPI_VERSION: z.string(),
|
||||
REDIS_PASSWORD: z.string(),
|
||||
POSTGRES_PORT: z.string(),
|
||||
POSTGRES_USERNAME: z.string(),
|
||||
POSTGRES_PASSWORD: z.string(),
|
||||
POSTGRES_DBNAME: z.string(),
|
||||
})
|
||||
.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 {
|
||||
storagePath: STORAGE_PATH,
|
||||
|
@ -28,6 +33,10 @@ const environmentSchema = z
|
|||
tipiVersion: TIPI_VERSION,
|
||||
internalIp: INTERNAL_IP,
|
||||
redisPassword: REDIS_PASSWORD,
|
||||
postgresPort: POSTGRES_PORT,
|
||||
postgresUsername: POSTGRES_USERNAME,
|
||||
postgresPassword: POSTGRES_PASSWORD,
|
||||
postgresDatabase: POSTGRES_DBNAME,
|
||||
...rest,
|
||||
};
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue