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
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
id: cpu-cores
uses: SimenB/github-actions-cpu-cores@v1

View file

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

View file

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

View file

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

View file

@ -1,9 +1,9 @@
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { appInfoSchema } from '@runtipi/shared';
import { appInfoSchema, envMapToString, envStringToMap } from '@runtipi/shared';
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';
/**

View file

@ -3,36 +3,6 @@ import fs from 'fs';
import path from 'path';
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.
* 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 { installApp, startApp, stopApp, uninstallApp, updateApp, regenerateAppEnv } = new AppExecutors();
const { cloneRepo, pullRepo } = new RepoExecutors();
const { systemInfo, restart } = new SystemExecutors();
const { systemInfo, restart, update } = new SystemExecutors();
const event = eventSchema.safeParse(jobData);
@ -62,6 +62,10 @@ const runCommand = async (jobData: unknown) => {
if (data.command === 'restart') {
({ success, message } = await restart());
}
if (data.command === 'update') {
({ success, message } = await update(data.version));
}
}
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",
"description": "",
"main": "src/index.ts",
"scripts": {},
"scripts": {
"lint": "eslint --ext .ts src",
"tsc": "tsc --noEmit"
},
"keywords": [],
"author": "",
"license": "ISC",

View file

@ -23,10 +23,16 @@ const repoCommandSchema = z.object({
const systemCommandSchema = z.object({
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({
success: z.boolean(),

View file

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

View file

@ -1,6 +1,4 @@
import fs from 'fs-extra';
import { App } from '@/server/db/schema';
import { getAppEnvMap } from '@/server/utils/env-generation';
import { appInfoSchema } from '@runtipi/shared';
import { fileExists, readdirSync, readFile, readJsonFile } from '../../common/fs.helpers';
import { getConfig } from '../../core/TipiConfig';
@ -32,44 +30,6 @@ export const checkAppRequirements = (appName: string) => {
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,
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');
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;
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);
}
return { current: TipiConfig.getConfig().version, latest: version?.replace('v', ''), body };
return { current: TipiConfig.getConfig().version, latest: version, body };
} catch (e) {
Logger.error(e);
return { current: TipiConfig.getConfig().version, latest: undefined };
@ -101,7 +101,7 @@ export class SystemServiceClass {
TipiConfig.setConfig('status', 'UPDATING');
this.dispatcher.dispatchEvent({ type: 'system', command: 'update' });
this.dispatcher.dispatchEvent({ type: 'system', command: 'update', version: latest });
return true;
};