feat: create app update event

This commit is contained in:
Nicolas Meienberger 2023-08-16 21:18:49 +02:00 committed by Nicolas Meienberger
parent 77dd73bdb4
commit 56c9d51d13
14 changed files with 83 additions and 94 deletions

View file

@ -62,6 +62,12 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: Run linter
run: pnpm run lint
- name: Run linter on packages
run: pnpm -r run lint
- name: Get number of CPU cores - name: Get number of CPU cores
id: cpu-cores id: cpu-cores
uses: SimenB/github-actions-cpu-cores@v1 uses: SimenB/github-actions-cpu-cores@v1

View file

@ -11,8 +11,8 @@ services:
command: --providers.docker command: --providers.docker
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
- ${PWD}/traefik:/root/.config - ./traefik:/root/.config
- ${PWD}/traefik/shared:/shared - ./traefik/shared:/shared
networks: networks:
- tipi_main_network - tipi_main_network
@ -22,7 +22,7 @@ services:
restart: on-failure restart: on-failure
stop_grace_period: 1m stop_grace_period: 1m
volumes: volumes:
- ${PWD}/data/postgres:/var/lib/postgresql/data - ./data/postgres:/var/lib/postgresql/data
environment: environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_USER: tipi POSTGRES_USER: tipi
@ -65,14 +65,14 @@ services:
env_file: env_file:
- .env - .env
environment: environment:
NODE_ENV: development NODE_ENV: production
volumes: volumes:
- ${PWD}/.env:/runtipi/.env - ./.env:/runtipi/.env
- ${PWD}/state:/runtipi/state - ./state:/runtipi/state
- ${PWD}/repos:/runtipi/repos:ro - ./repos:/runtipi/repos:ro
- ${PWD}/apps:/runtipi/apps - ./apps:/runtipi/apps
- ${PWD}/logs:/app/logs - ./logs:/app/logs
- ${PWD}/traefik:/runtipi/traefik - ./traefik:/runtipi/traefik
- ${STORAGE_PATH}:/app/storage - ${STORAGE_PATH}:/app/storage
labels: labels:
# Main # Main

View file

@ -8,7 +8,7 @@ async function bundle() {
entryPoints: ['./src/index.ts'], entryPoints: ['./src/index.ts'],
outfile: './dist/index.js', outfile: './dist/index.js',
platform: 'node', platform: 'node',
target: 'node20', target: 'node18',
bundle: true, bundle: true,
color: true, color: true,
sourcemap: commandArgs.includes('--sourcemap'), sourcemap: commandArgs.includes('--sourcemap'),

View file

@ -13,7 +13,8 @@
"build": "node build.js", "build": "node build.js",
"build:meta": "esbuild ./src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --metafile=meta.json --analyze", "build:meta": "esbuild ./src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --metafile=meta.json --analyze",
"dev": "dotenv -e ../../.env nodemon", "dev": "dotenv -e ../../.env nodemon",
"lint": "eslint . --ext .ts" "lint": "eslint . --ext .ts",
"tsc": "tsc --noEmit"
}, },
"pkg": { "pkg": {
"assets": "assets/**/*", "assets": "assets/**/*",

View file

@ -1,9 +1,9 @@
import crypto from 'crypto'; import crypto from 'crypto';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { appInfoSchema } from '@runtipi/shared'; import { appInfoSchema, envMapToString, envStringToMap } from '@runtipi/shared';
import { getEnv } from '@/utils/environment/environment'; import { getEnv } from '@/utils/environment/environment';
import { envMapToString, envStringToMap, generateVapidKeys, getAppEnvMap } from './env.helpers'; import { generateVapidKeys, getAppEnvMap } from './env.helpers';
import { pathExists } from '@/utils/fs-helpers'; import { pathExists } from '@/utils/fs-helpers';
/** /**

View file

@ -3,36 +3,6 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import { getEnv } from '@/utils/environment/environment'; import { getEnv } from '@/utils/environment/environment';
/**
* Convert a string of environment variables to a Map
*
* @param {string} envString - String of environment variables
*/
export const envStringToMap = (envString: string) => {
const envMap = new Map<string, string>();
const envArray = envString.split('\n');
envArray.forEach((env) => {
const [key, value] = env.split('=');
if (key && value) {
envMap.set(key, value);
}
});
return envMap;
};
/**
* Convert a Map of environment variables to a valid string of environment variables
* that can be used in a .env file
*
* @param {Map<string, string>} envMap - Map of environment variables
*/
export const envMapToString = (envMap: Map<string, string>) => {
const envArray = Array.from(envMap).map(([key, value]) => `${key}=${value}`);
return envArray.join('\n');
};
/** /**
* This function reads the env file for the app with the provided id and returns a Map containing the key-value pairs of the environment variables. * This function reads the env file for the app with the provided id and returns a Map containing the key-value pairs of the environment variables.
* It reads the app.env file, splits it into individual environment variables, and stores them in a Map, with the environment variable name as the key and its value as the value. * It reads the app.env file, splits it into individual environment variables, and stores them in a Map, with the environment variable name as the key and its value as the value.

View file

@ -9,7 +9,7 @@ const execAsync = promisify(exec);
const runCommand = async (jobData: unknown) => { const runCommand = async (jobData: unknown) => {
const { installApp, startApp, stopApp, uninstallApp, updateApp, regenerateAppEnv } = new AppExecutors(); const { installApp, startApp, stopApp, uninstallApp, updateApp, regenerateAppEnv } = new AppExecutors();
const { cloneRepo, pullRepo } = new RepoExecutors(); const { cloneRepo, pullRepo } = new RepoExecutors();
const { systemInfo, restart } = new SystemExecutors(); const { systemInfo, restart, update } = new SystemExecutors();
const event = eventSchema.safeParse(jobData); const event = eventSchema.safeParse(jobData);
@ -62,6 +62,10 @@ const runCommand = async (jobData: unknown) => {
if (data.command === 'restart') { if (data.command === 'restart') {
({ success, message } = await restart()); ({ success, message } = await restart());
} }
if (data.command === 'update') {
({ success, message } = await update(data.version));
}
} }
return { success, message }; return { success, message };

View file

@ -0,0 +1 @@
.eslintrc.js

View file

@ -0,0 +1,39 @@
module.exports = {
root: true,
plugins: ['@typescript-eslint', 'import'],
extends: ['plugin:@typescript-eslint/recommended', 'airbnb', 'airbnb-typescript', 'eslint:recommended', 'plugin:import/typescript', 'prettier'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
rules: {
'import/prefer-default-export': 0,
'class-methods-use-this': 0,
'import/extensions': [
'error',
'ignorePackages',
{
'': 'never',
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: ['build.js', '**/*.test.{ts,tsx}', '**/mocks/**', '**/__mocks__/**', '**/*.setup.{ts,js}', '**/*.config.{ts,js}', '**/tests/**'],
},
],
'arrow-body-style': 0,
'no-underscore-dangle': 0,
'no-console': 0,
},
globals: {
NodeJS: true,
},
};

View file

@ -3,7 +3,10 @@
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "src/index.ts", "main": "src/index.ts",
"scripts": {}, "scripts": {
"lint": "eslint --ext .ts src",
"tsc": "tsc --noEmit"
},
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",

View file

@ -23,10 +23,16 @@ const repoCommandSchema = z.object({
const systemCommandSchema = z.object({ const systemCommandSchema = z.object({
type: z.literal(EVENT_TYPES.SYSTEM), type: z.literal(EVENT_TYPES.SYSTEM),
command: z.union([z.literal('restart'), z.literal('update'), z.literal('system_info')]), command: z.union([z.literal('restart'), z.literal('system_info')]),
}); });
export const eventSchema = appCommandSchema.or(repoCommandSchema).or(systemCommandSchema); const updateSchema = z.object({
type: z.literal(EVENT_TYPES.SYSTEM),
command: z.literal('update'),
version: z.string(),
});
export const eventSchema = appCommandSchema.or(repoCommandSchema).or(systemCommandSchema).or(updateSchema);
export const eventResultSchema = z.object({ export const eventResultSchema = z.object({
success: z.boolean(), success: z.boolean(),

View file

@ -19,7 +19,6 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",
"incremental": true,
"strictNullChecks": true, "strictNullChecks": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,

View file

@ -1,6 +1,4 @@
import fs from 'fs-extra';
import { App } from '@/server/db/schema'; import { App } from '@/server/db/schema';
import { getAppEnvMap } from '@/server/utils/env-generation';
import { appInfoSchema } from '@runtipi/shared'; import { appInfoSchema } from '@runtipi/shared';
import { fileExists, readdirSync, readFile, readJsonFile } from '../../common/fs.helpers'; import { fileExists, readdirSync, readFile, readJsonFile } from '../../common/fs.helpers';
import { getConfig } from '../../core/TipiConfig'; import { getConfig } from '../../core/TipiConfig';
@ -32,44 +30,6 @@ export const checkAppRequirements = (appName: string) => {
return parsedConfig.data; return parsedConfig.data;
}; };
/**
* This function checks if the env file for the app with the provided name is valid.
* It reads the config.json file for the app, parses it,
* and uses the app's form fields to check if all required fields are present in the env file.
* If the config.json file is invalid, it throws an error.
* If a required variable is missing in the env file, it throws an error.
*
* @param {string} appName - The name of the app.
* @throws Will throw an error if the app has an invalid config.json file or if a required variable is missing in the env file.
*/
export const checkEnvFile = async (appName: string) => {
const configFile = await fs.promises.readFile(`/runtipi/apps/${appName}/config.json`);
let jsonConfig: unknown;
try {
jsonConfig = JSON.parse(configFile.toString());
} catch (e) {
throw new Error(`App ${appName} has invalid config.json file`);
}
const parsedConfig = appInfoSchema.safeParse(jsonConfig);
if (!parsedConfig.success) {
throw new Error(`App ${appName} has invalid config.json file`);
}
const envMap = await getAppEnvMap(appName);
parsedConfig.data.form_fields.forEach((field) => {
const envVar = field.env_variable;
const envVarValue = envMap.get(envVar);
if (!envVarValue && field.required) {
throw new Error('New info needed. App config needs to be updated');
}
});
};
/** /**
This function reads the apps directory and skips certain system files, then reads the config.json and metadata/description.md files for each app, This function reads the apps directory and skips certain system files, then reads the config.json and metadata/description.md files for each app,
parses the config file, filters out any apps that are not available and returns an array of app information. parses the config file, filters out any apps that are not available and returns an array of app information.

View file

@ -48,16 +48,16 @@ export class SystemServiceClass {
let body = await this.cache.get('latestVersionBody'); let body = await this.cache.get('latestVersionBody');
if (!version) { if (!version) {
const { data } = await axios.get<{ name: string; body: string }>('https://api.github.com/repos/meienberger/runtipi/releases/latest'); const { data } = await axios.get<{ tag_name: string; body: string }>('https://api.github.com/repos/meienberger/runtipi/releases/latest');
version = data.name.replace('v', ''); version = data.tag_name;
body = data.body; body = data.body;
await this.cache.set('latestVersion', version?.replace('v', '') || '', 60 * 60); await this.cache.set('latestVersion', version || '', 60 * 60);
await this.cache.set('latestVersionBody', body || '', 60 * 60); await this.cache.set('latestVersionBody', body || '', 60 * 60);
} }
return { current: TipiConfig.getConfig().version, latest: version?.replace('v', ''), body }; return { current: TipiConfig.getConfig().version, latest: version, body };
} catch (e) { } catch (e) {
Logger.error(e); Logger.error(e);
return { current: TipiConfig.getConfig().version, latest: undefined }; return { current: TipiConfig.getConfig().version, latest: undefined };
@ -101,7 +101,7 @@ export class SystemServiceClass {
TipiConfig.setConfig('status', 'UPDATING'); TipiConfig.setConfig('status', 'UPDATING');
this.dispatcher.dispatchEvent({ type: 'system', command: 'update' }); this.dispatcher.dispatchEvent({ type: 'system', command: 'update', version: latest });
return true; return true;
}; };