refactor: replace usage of config with new runtime config

wip: make script executable from everywhere
This commit is contained in:
Nicolas Meienberger 2022-09-21 09:06:38 +02:00
parent bd881711f8
commit 78cb3c36ad
32 changed files with 272 additions and 297 deletions

View file

@ -3,7 +3,7 @@ on:
push:
env:
ROOT_FOLDER: /test
ROOT_FOLDER: /runtipi
JWT_SECRET: "secret"
ROOT_FOLDER_HOST: /tipi
APPS_REPO_ID: repo-id

View file

@ -53,7 +53,7 @@ services:
volumes:
## Docker sock
- /var/run/docker.sock:/var/run/docker.sock:ro
- ${PWD}:/tipi
- ${PWD}:/runtipi
- ${PWD}/packages/system-api/src:/api/src
# - /api/node_modules
environment:

View file

@ -46,7 +46,7 @@ services:
volumes:
## Docker sock
- /var/run/docker.sock:/var/run/docker.sock:ro
- ${PWD}:/tipi
- ${PWD}:/runtipi
environment:
INTERNAL_IP: ${INTERNAL_IP}
TIPI_VERSION: ${TIPI_VERSION}

View file

@ -46,7 +46,7 @@ services:
volumes:
## Docker sock
- /var/run/docker.sock:/var/run/docker.sock:ro
- ${PWD}:/tipi
- ${PWD}:/runtipi
environment:
INTERNAL_IP: ${INTERNAL_IP}
TIPI_VERSION: ${TIPI_VERSION}

View file

@ -1,5 +1,5 @@
/** @type {import('next').NextConfig} */
const { INTERNAL_IP, DOMAIN } = process.env;
const { INTERNAL_IP, DOMAIN, NGINX_PORT } = process.env;
const nextConfig = {
webpackDevMiddleware: (config) => {
@ -11,8 +11,9 @@ const nextConfig = {
},
reactStrictMode: true,
env: {
INTERNAL_IP: INTERNAL_IP,
NEXT_PUBLIC_INTERNAL_IP: INTERNAL_IP,
NEXT_PUBLIC_DOMAIN: DOMAIN,
NEXT_PUBLIC_PORT: NGINX_PORT,
},
basePath: '/dashboard',
};

View file

@ -2,8 +2,8 @@ export const getUrl = (url: string) => {
const domain = process.env.NEXT_PUBLIC_DOMAIN;
let prefix = '';
prefix = 'dashboard';
if (domain !== 'tipi.localhost') {
prefix = 'dashboard';
}
return `/${prefix}/${url}`;

View file

@ -1,22 +1,23 @@
import { useEffect, useState } from 'react';
import { ApolloClient } from '@apollo/client';
import axios from 'axios';
import useSWR, { BareFetcher } from 'swr';
import { createApolloClient } from '../core/apollo/client';
import { useSytemStore } from '../state/systemStore';
import { getUrl } from '../core/helpers/url-helpers';
interface IReturnProps {
client?: ApolloClient<unknown>;
isLoadingComplete?: boolean;
}
const fetcher: BareFetcher<any> = (url: string) => {
return axios.get(getUrl(url)).then((res) => res.data);
};
// const fetcher: BareFetcher<any> = (url: string) => {
// return axios.get(getUrl(url)).then((res) => res.data);
// };
export default function useCachedResources(): IReturnProps {
const { data } = useSWR<{ ip: string; domain: string; port: string }>('api/ip', fetcher);
const ip = process.env.NEXT_PUBLIC_INTERNAL_IP;
const domain = process.env.NEXT_PUBLIC_DOMAIN;
const port = process.env.NEXT_PUBLIC_PORT;
// const { data } = useSWR<{ ip: string; domain: string; port: string }>('api/ip', fetcher);
const { baseUrl, setBaseUrl, setInternalIp, setDomain } = useSytemStore();
const [isLoadingComplete, setLoadingComplete] = useState(false);
const [client, setClient] = useState<ApolloClient<unknown>>();
@ -35,7 +36,6 @@ export default function useCachedResources(): IReturnProps {
}
useEffect(() => {
const { ip, domain, port } = data || {};
if (ip && !baseUrl) {
setInternalIp(ip);
setDomain(domain);
@ -50,7 +50,7 @@ export default function useCachedResources(): IReturnProps {
setBaseUrl(`https://${domain}/api`);
}
}
}, [baseUrl, setBaseUrl, data, setInternalIp, setDomain]);
}, [baseUrl, setBaseUrl, setInternalIp, setDomain]);
useEffect(() => {
if (baseUrl) {

View file

@ -1,58 +0,0 @@
import * as dotenv from 'dotenv';
interface IConfig {
logs: {
LOGS_FOLDER: string;
LOGS_APP: string;
LOGS_ERROR: string;
};
NODE_ENV: string;
ROOT_FOLDER: string;
JWT_SECRET: string;
CLIENT_URLS: string[];
VERSION: string;
ROOT_FOLDER_HOST: string;
APPS_REPO_ID: string;
APPS_REPO_URL: string;
INTERNAL_IP: string;
}
if (process.env.NODE_ENV !== 'production') {
dotenv.config({ path: '.env.dev' });
} else {
dotenv.config({ path: '.env' });
}
const {
LOGS_FOLDER = 'logs',
LOGS_APP = 'app.log',
LOGS_ERROR = 'error.log',
NODE_ENV = 'development',
JWT_SECRET = '',
INTERNAL_IP = '',
TIPI_VERSION = '',
ROOT_FOLDER_HOST = '',
NGINX_PORT = '80',
APPS_REPO_ID = '',
APPS_REPO_URL = '',
DOMAIN = '',
} = process.env;
const config: IConfig = {
logs: {
LOGS_FOLDER,
LOGS_APP,
LOGS_ERROR,
},
NODE_ENV,
ROOT_FOLDER: '/tipi',
JWT_SECRET,
CLIENT_URLS: ['http://localhost:3000', `http://${INTERNAL_IP}`, `http://${INTERNAL_IP}:${NGINX_PORT}`, `http://${INTERNAL_IP}:3000`, `https://${DOMAIN}`],
VERSION: TIPI_VERSION,
ROOT_FOLDER_HOST,
APPS_REPO_ID,
APPS_REPO_URL,
INTERNAL_IP,
};
export default config;

View file

@ -1 +0,0 @@
export { default } from './config';

View file

@ -1,13 +1,15 @@
import fs from 'fs-extra';
import path from 'path';
import { createLogger, format, transports } from 'winston';
import config from '..';
import { getConfig } from '../../core/config/TipiConfig';
const { logs, NODE_ENV } = getConfig();
const { align, printf, timestamp, combine, colorize } = format;
// Create the logs directory if it does not exist
if (!fs.existsSync(config.logs.LOGS_FOLDER)) {
fs.mkdirSync(config.logs.LOGS_FOLDER);
if (!fs.existsSync(logs.LOGS_FOLDER)) {
fs.mkdirSync(logs.LOGS_FOLDER);
}
/**
@ -36,14 +38,14 @@ const Logger = createLogger({
// - Write all logs error (and below) to `error.log`.
//
new transports.File({
filename: path.join(config.logs.LOGS_FOLDER, config.logs.LOGS_ERROR),
filename: path.join(logs.LOGS_FOLDER, logs.LOGS_ERROR),
level: 'error',
}),
new transports.File({
filename: path.join(config.logs.LOGS_FOLDER, config.logs.LOGS_APP),
filename: path.join(logs.LOGS_FOLDER, logs.LOGS_APP),
}),
],
exceptionHandlers: [new transports.File({ filename: path.join(config.logs.LOGS_FOLDER, config.logs.LOGS_ERROR) })],
exceptionHandlers: [new transports.File({ filename: path.join(logs.LOGS_FOLDER, logs.LOGS_ERROR) })],
});
//
@ -59,4 +61,4 @@ const LoggerDev = createLogger({
],
});
export default config.NODE_ENV === 'production' ? Logger : LoggerDev;
export default NODE_ENV === 'production' ? Logger : LoggerDev;

View file

@ -1,5 +1,5 @@
import config from '../config';
import { getConfig } from '../core/config/TipiConfig';
export const APP_DATA_FOLDER = 'app-data';
export const APPS_FOLDER = 'apps';
export const isProd = config.NODE_ENV === 'production';
export const isProd = getConfig().NODE_ENV === 'production';

View file

@ -1,7 +1,6 @@
import { z } from 'zod';
import * as dotenv from 'dotenv';
import fs from 'fs-extra';
import config from '../../config';
import { readJsonFile } from '../../modules/fs/fs.helpers';
if (process.env.NODE_ENV !== 'production') {
@ -24,13 +23,13 @@ const {
} = process.env;
const configSchema = z.object({
NODE_ENV: z.string(),
repo: z.string(),
NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]),
logs: z.object({
LOGS_FOLDER: z.string(),
LOGS_APP: z.string(),
LOGS_ERROR: z.string(),
}),
dnsIp: z.string(),
rootFolder: z.string(),
internalIp: z.string(),
version: z.string(),
@ -47,28 +46,26 @@ class Config {
private config: z.infer<typeof configSchema>;
constructor() {
const fileConfig = readJsonFile('/tipi/state/settings.json');
const envConfig: z.infer<typeof configSchema> = {
logs: {
LOGS_FOLDER,
LOGS_APP,
LOGS_ERROR,
},
NODE_ENV,
repo: APPS_REPO_URL,
rootFolder: '/tipi',
NODE_ENV: NODE_ENV as z.infer<typeof configSchema>['NODE_ENV'],
rootFolder: '/runtipi',
internalIp: INTERNAL_IP,
version: TIPI_VERSION,
jwtSecret: JWT_SECRET,
clientUrls: ['http://localhost:3000', `http://${INTERNAL_IP}`, `http://${INTERNAL_IP}:${NGINX_PORT}`, `http://${INTERNAL_IP}:3000`, `https://${DOMAIN}`],
clientUrls: ['http://localhost:3000', `http://${INTERNAL_IP}`, `http://${INTERNAL_IP}:${NGINX_PORT}`, `http://${INTERNAL_IP}:3000`, DOMAIN && `https://${DOMAIN}`].filter(Boolean),
appsRepoId: APPS_REPO_ID,
appsRepoUrl: APPS_REPO_URL,
domain: DOMAIN,
dnsIp: '9.9.9.9',
};
const parsed = configSchema.parse({
...envConfig,
...fileConfig,
});
this.config = parsed;
@ -85,13 +82,24 @@ class Config {
return this.config;
}
public applyJsonConfig() {
const fileConfig = readJsonFile('/state/settings.json');
const parsed = configSchema.parse({
...this.config,
...fileConfig,
});
this.config = parsed;
}
public setConfig(key: keyof typeof configSchema.shape, value: any) {
const newConf = { ...this.getConfig() };
newConf[key] = value;
this.config = configSchema.parse(newConf);
fs.writeFileSync(`${config.ROOT_FOLDER}/state/settings.json`, JSON.stringify(newConf));
fs.writeFileSync(`${this.config.rootFolder}/state/settings.json`, JSON.stringify(newConf));
}
}
@ -100,3 +108,5 @@ export const setConfig = (key: keyof typeof configSchema.shape, value: any) => {
};
export const getConfig = () => Config.getInstance().getConfig();
export const applyJsonConfig = () => Config.getInstance().applyJsonConfig();

View file

@ -1,14 +1,14 @@
import cron from 'node-cron';
import config from '../../config';
import logger from '../../config/logger/logger';
import { updateRepo } from '../../helpers/repo-helpers';
import { getConfig } from '../../core/config/TipiConfig';
const startJobs = () => {
logger.info('Starting cron jobs...');
cron.schedule('0 * * * *', () => {
logger.info('Cloning apps repo...');
updateRepo(config.APPS_REPO_URL);
updateRepo(getConfig().appsRepoUrl);
});
};

View file

@ -1,7 +1,7 @@
import session from 'express-session';
import config from '../../config';
import SessionFileStore from 'session-file-store';
import { COOKIE_MAX_AGE, __prod__ } from '../../config/constants/constants';
import { getConfig } from '../config/TipiConfig';
const getSessionMiddleware = () => {
const FileStore = SessionFileStore(session);
@ -12,7 +12,7 @@ const getSessionMiddleware = () => {
name: 'qid',
store: new FileStore(),
cookie: { maxAge: COOKIE_MAX_AGE, secure: false, sameSite, httpOnly: true },
secret: config.JWT_SECRET,
secret: getConfig().jwtSecret,
resave: false,
saveUninitialized: false,
});

View file

@ -6,6 +6,7 @@ import { AppInfo, AppStatusEnum } from '../../../modules/apps/apps.types';
import { createApp } from '../../../modules/apps/__tests__/apps.factory';
import Update, { UpdateStatusEnum } from '../../../modules/system/update.entity';
import { setupConnection, teardownConnection } from '../../../test/connection';
import { getConfig } from '../../config/TipiConfig';
import { updateV040 } from '../v040';
jest.mock('fs');
@ -61,7 +62,7 @@ describe('No state/apps.json', () => {
describe('State/apps.json exists with no installed app', () => {
beforeEach(async () => {
const { MockFiles } = await createApp({});
MockFiles['/tipi/state/apps.json'] = createState([]);
MockFiles[`${getConfig().rootFolder}/state/apps.json`] = createState([]);
// @ts-ignore
fs.__createMockFiles(MockFiles);
});
@ -79,7 +80,7 @@ describe('State/apps.json exists with no installed app', () => {
it('Should delete state file after update', async () => {
await updateV040();
expect(fs.existsSync('/tipi/state/apps.json')).toBe(false);
expect(fs.existsSync(`${getConfig().rootFolder}/state/apps.json`)).toBe(false);
});
});
@ -88,9 +89,9 @@ describe('State/apps.json exists with one installed app', () => {
beforeEach(async () => {
const { MockFiles, appInfo } = await createApp({});
app1 = appInfo;
MockFiles['/tipi/state/apps.json'] = createState([appInfo.id]);
MockFiles[`/tipi/app-data/${appInfo.id}`] = '';
MockFiles[`/tipi/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
MockFiles[`${getConfig().rootFolder}/state/apps.json`] = createState([appInfo.id]);
MockFiles[`${getConfig().rootFolder}/app-data/${appInfo.id}`] = '';
MockFiles[`${getConfig().rootFolder}/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
// @ts-ignore
fs.__createMockFiles(MockFiles);
});
@ -117,9 +118,9 @@ describe('State/apps.json exists with one installed app', () => {
it('Should not try to migrate app if it already exists', async () => {
const { MockFiles, appInfo } = await createApp({ installed: true });
app1 = appInfo;
MockFiles['/tipi/state/apps.json'] = createState([appInfo.id]);
MockFiles[`/tipi/app-data/${appInfo.id}`] = '';
MockFiles[`/tipi/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
MockFiles[`${getConfig().rootFolder}/state/apps.json`] = createState([appInfo.id]);
MockFiles[`${getConfig().rootFolder}/app-data/${appInfo.id}`] = '';
MockFiles[`${getConfig().rootFolder}/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
// @ts-ignore
fs.__createMockFiles(MockFiles);

View file

@ -1,10 +1,10 @@
import config from '../../config';
import logger from '../../config/logger/logger';
import App from '../../modules/apps/app.entity';
import { AppInfo, AppStatusEnum } from '../../modules/apps/apps.types';
import User from '../../modules/auth/user.entity';
import { deleteFolder, fileExists, readFile, readJsonFile } from '../../modules/fs/fs.helpers';
import Update, { UpdateStatusEnum } from '../../modules/system/update.entity';
import { getConfig } from '../config/TipiConfig';
type AppsState = { installed: string };
@ -39,7 +39,7 @@ export const updateV040 = async (): Promise<void> => {
const form: Record<string, string> = {};
const configFile: AppInfo | null = readJsonFile(`/repos/${config.APPS_REPO_ID}/apps/${appId}/config.json`);
const configFile: AppInfo | null = readJsonFile(`/repos/${getConfig().appsRepoId}/apps/${appId}/config.json`);
configFile?.form_fields?.forEach((field) => {
const envVar = field.env_variable;
const envVarValue = envVarsMap.get(envVar);

View file

@ -1,7 +1,7 @@
import { faker } from '@faker-js/faker';
import { AppCategoriesEnum, AppInfo, AppStatusEnum, FieldTypes } from '../apps.types';
import config from '../../../config';
import App from '../app.entity';
import { getConfig } from '../../../core/config/TipiConfig';
interface IProps {
installed?: boolean;
@ -55,11 +55,11 @@ const createApp = async (props: IProps) => {
}
let MockFiles: any = {};
MockFiles[`${config.ROOT_FOLDER}/.env`] = 'TEST=test';
MockFiles[`${config.ROOT_FOLDER}/repos/repo-id`] = '';
MockFiles[`${config.ROOT_FOLDER}/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
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';
MockFiles[`${getConfig().rootFolder}/.env`] = 'TEST=test';
MockFiles[`${getConfig().rootFolder}/repos/repo-id`] = '';
MockFiles[`${getConfig().rootFolder}/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
MockFiles[`${getConfig().rootFolder}/repos/repo-id/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
MockFiles[`${getConfig().rootFolder}/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
let appEntity = new App();
if (installed) {
@ -71,10 +71,10 @@ const createApp = async (props: IProps) => {
domain,
}).save();
MockFiles[`${config.ROOT_FOLDER}/app-data/${appInfo.id}`] = '';
MockFiles[`${config.ROOT_FOLDER}/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
MockFiles[`${config.ROOT_FOLDER}/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
MockFiles[`${config.ROOT_FOLDER}/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
MockFiles[`${getConfig().rootFolder}/app-data/${appInfo.id}`] = '';
MockFiles[`${getConfig().rootFolder}/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
MockFiles[`${getConfig().rootFolder}/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
MockFiles[`${getConfig().rootFolder}/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
}
return { appInfo, MockFiles, appEntity };

View file

@ -1,7 +1,7 @@
import { faker } from '@faker-js/faker';
import fs from 'fs-extra';
import { DataSource } from 'typeorm';
import config from '../../../config';
import { getConfig } from '../../../core/config/TipiConfig';
import { setupConnection, teardownConnection } from '../../../test/connection';
import App from '../app.entity';
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo, runAppScript } from '../apps.helpers';
@ -95,7 +95,7 @@ describe('checkEnvFile', () => {
it('Should throw if a required field is missing', () => {
const newAppEnv = 'APP_PORT=test\n';
fs.writeFileSync(`${config.ROOT_FOLDER}/app-data/${app1.id}/app.env`, newAppEnv);
fs.writeFileSync(`${getConfig().rootFolder}/app-data/${app1.id}/app.env`, newAppEnv);
try {
checkEnvFile(app1.id);
@ -167,7 +167,7 @@ describe('generateEnvFile', () => {
const randomField = faker.random.alphaNumeric(32);
fs.writeFileSync(`${config.ROOT_FOLDER}/app-data/${appInfo.id}/app.env`, `RANDOM_FIELD=${randomField}`);
fs.writeFileSync(`${getConfig().rootFolder}/app-data/${appInfo.id}/app.env`, `RANDOM_FIELD=${randomField}`);
generateEnvFile(appEntity);
@ -271,7 +271,7 @@ describe('getAppInfo', () => {
// @ts-ignore
fs.__createMockFiles(MockFiles);
fs.writeFileSync(`${config.ROOT_FOLDER}/repos/repo-id/apps/${app1.id}/config.json`, '{}');
fs.writeFileSync(`${getConfig().rootFolder}/repos/repo-id/apps/${app1.id}/config.json`, '{}');
const app = await getAppInfo(appInfo.id);

View file

@ -1,6 +1,5 @@
import AppsService from '../apps.service';
import fs from 'fs-extra';
import config from '../../../config';
import childProcess from 'child_process';
import { AppInfo, AppStatusEnum } from '../apps.types';
import App from '../app.entity';
@ -8,6 +7,7 @@ import { createApp } from './apps.factory';
import { setupConnection, teardownConnection } from '../../../test/connection';
import { DataSource } from 'typeorm';
import { getEnvMap } from '../apps.helpers';
import { getConfig } from '../../../core/config/TipiConfig';
jest.mock('fs-extra');
jest.mock('child_process');
@ -43,7 +43,7 @@ describe('Install app', () => {
it('Should correctly generate env file for app', async () => {
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/${app1.id}/app.env`).toString();
const envFile = fs.readFileSync(`${getConfig().rootFolder}/app-data/${app1.id}/app.env`).toString();
expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test\nAPP_DOMAIN=192.168.1.10:${app1.port}`);
});
@ -63,7 +63,7 @@ describe('Install app', () => {
const spy = jest.spyOn(childProcess, 'execFile');
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['install', app1.id], {}, expect.any(Function)]);
expect(spy.mock.lastCall).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['install', app1.id], {}, expect.any(Function)]);
spy.mockRestore();
});
@ -74,8 +74,8 @@ describe('Install app', () => {
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
expect(spy.mock.calls.length).toBe(2);
expect(spy.mock.calls[0]).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['install', app1.id], {}, expect.any(Function)]);
expect(spy.mock.calls[1]).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['start', app1.id], {}, expect.any(Function)]);
expect(spy.mock.calls[0]).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['install', app1.id], {}, expect.any(Function)]);
expect(spy.mock.calls[1]).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['start', app1.id], {}, expect.any(Function)]);
spy.mockRestore();
});
@ -112,7 +112,7 @@ describe('Install app', () => {
it('Should correctly copy app from repos to apps folder', async () => {
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
const appFolder = fs.readdirSync(`${config.ROOT_FOLDER}/apps/${app1.id}`);
const appFolder = fs.readdirSync(`${getConfig().rootFolder}/apps/${app1.id}`);
expect(appFolder).toBeDefined();
expect(appFolder.indexOf('docker-compose.yml')).toBeGreaterThanOrEqual(0);
@ -121,19 +121,19 @@ describe('Install app', () => {
it('Should cleanup any app folder existing before install', async () => {
const { MockFiles, appInfo } = await createApp({});
app1 = appInfo;
MockFiles[`/tipi/apps/${appInfo.id}/docker-compose.yml`] = 'test';
MockFiles[`/tipi/apps/${appInfo.id}/test.yml`] = 'test';
MockFiles[`/tipi/apps/${appInfo.id}`] = ['test.yml', 'docker-compose.yml'];
MockFiles[`${getConfig().rootFolder}/apps/${appInfo.id}/docker-compose.yml`] = 'test';
MockFiles[`${getConfig().rootFolder}/apps/${appInfo.id}/test.yml`] = 'test';
MockFiles[`${getConfig().rootFolder}/apps/${appInfo.id}`] = ['test.yml', 'docker-compose.yml'];
// @ts-ignore
fs.__createMockFiles(MockFiles);
expect(fs.existsSync(`${config.ROOT_FOLDER}/apps/${app1.id}/test.yml`)).toBe(true);
expect(fs.existsSync(`${getConfig().rootFolder}/apps/${app1.id}/test.yml`)).toBe(true);
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
expect(fs.existsSync(`${config.ROOT_FOLDER}/apps/${app1.id}/test.yml`)).toBe(false);
expect(fs.existsSync(`${config.ROOT_FOLDER}/apps/${app1.id}/docker-compose.yml`)).toBe(true);
expect(fs.existsSync(`${getConfig().rootFolder}/apps/${app1.id}/test.yml`)).toBe(false);
expect(fs.existsSync(`${getConfig().rootFolder}/apps/${app1.id}/docker-compose.yml`)).toBe(true);
});
it('Should throw if app is exposed and domain is not provided', async () => {
@ -194,7 +194,7 @@ describe('Uninstall app', () => {
await AppsService.uninstallApp(app1.id);
expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['uninstall', app1.id], {}, expect.any(Function)]);
expect(spy.mock.lastCall).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['uninstall', app1.id], {}, expect.any(Function)]);
spy.mockRestore();
});
@ -205,8 +205,8 @@ describe('Uninstall app', () => {
await AppsService.uninstallApp(app1.id);
expect(spy.mock.calls.length).toBe(2);
expect(spy.mock.calls[0]).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['stop', app1.id], {}, expect.any(Function)]);
expect(spy.mock.calls[1]).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['uninstall', app1.id], {}, expect.any(Function)]);
expect(spy.mock.calls[0]).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['stop', app1.id], {}, expect.any(Function)]);
expect(spy.mock.calls[1]).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['uninstall', app1.id], {}, expect.any(Function)]);
spy.mockRestore();
});
@ -245,7 +245,7 @@ describe('Start app', () => {
await AppsService.startApp(app1.id);
expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['start', app1.id], {}, expect.any(Function)]);
expect(spy.mock.lastCall).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['start', app1.id], {}, expect.any(Function)]);
spy.mockRestore();
});
@ -266,11 +266,11 @@ describe('Start app', () => {
});
it('Regenerate env file', async () => {
fs.writeFile(`${config.ROOT_FOLDER}/app-data/${app1.id}/app.env`, 'TEST=test\nAPP_PORT=3000', () => {});
fs.writeFile(`${getConfig().rootFolder}/app-data/${app1.id}/app.env`, 'TEST=test\nAPP_PORT=3000', () => {});
await AppsService.startApp(app1.id);
const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/${app1.id}/app.env`).toString();
const envFile = fs.readFileSync(`${getConfig().rootFolder}/app-data/${app1.id}/app.env`).toString();
expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test\nAPP_DOMAIN=192.168.1.10:${app1.port}`);
});
@ -302,7 +302,7 @@ describe('Stop app', () => {
await AppsService.stopApp(app1.id);
expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['stop', app1.id], {}, expect.any(Function)]);
expect(spy.mock.lastCall).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['stop', app1.id], {}, expect.any(Function)]);
});
it('Should throw if app is not installed', async () => {
@ -334,7 +334,7 @@ describe('Update app config', () => {
it('Should correctly update app config', async () => {
await AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' });
const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/${app1.id}/app.env`).toString();
const envFile = fs.readFileSync(`${getConfig().rootFolder}/app-data/${app1.id}/app.env`).toString();
expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test\nAPP_DOMAIN=192.168.1.10:${app1.port}`);
});
@ -352,8 +352,8 @@ describe('Update app config', () => {
// @ts-ignore
fs.__createMockFiles(MockFiles);
const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/${appInfo.id}/app.env`).toString();
fs.writeFileSync(`${config.ROOT_FOLDER}/app-data/${appInfo.id}/app.env`, `${envFile}\nRANDOM_FIELD=test`);
const envFile = fs.readFileSync(`${getConfig().rootFolder}/app-data/${appInfo.id}/app.env`).toString();
fs.writeFileSync(`${getConfig().rootFolder}/app-data/${appInfo.id}/app.env`, `${envFile}\nRANDOM_FIELD=test`);
await AppsService.updateAppConfig(appInfo.id, { TEST_FIELD: 'test' });
@ -470,8 +470,8 @@ describe('Start all apps', () => {
expect(spy.mock.calls.length).toBe(2);
expect(spy.mock.calls).toEqual([
[`${config.ROOT_FOLDER}/scripts/app.sh`, ['start', app1.id], {}, expect.any(Function)],
[`${config.ROOT_FOLDER}/scripts/app.sh`, ['start', app2.id], {}, expect.any(Function)],
[`${getConfig().rootFolder}/scripts/app.sh`, ['start', app1.id], {}, expect.any(Function)],
[`${getConfig().rootFolder}/scripts/app.sh`, ['start', app2.id], {}, expect.any(Function)],
]);
});

View file

@ -2,15 +2,17 @@ import portUsed from 'tcp-port-used';
import { fileExists, getSeed, readdirSync, readFile, readJsonFile, runScript, writeFile } from '../fs/fs.helpers';
import InternalIp from 'internal-ip';
import crypto from 'crypto';
import config from '../../config';
import { AppInfo, AppStatusEnum } from './apps.types';
import logger from '../../config/logger/logger';
import App from './app.entity';
import { getConfig } from '../../core/config/TipiConfig';
const { appsRepoId, internalIp } = getConfig();
export const checkAppRequirements = async (appName: string) => {
let valid = true;
const configFile: AppInfo | null = readJsonFile(`/repos/${config.APPS_REPO_ID}/apps/${appName}/config.json`);
const configFile: AppInfo | null = readJsonFile(`/repos/${appsRepoId}/apps/${appName}/config.json`);
if (!configFile) {
throw new Error(`App ${appName} not found`);
@ -110,7 +112,7 @@ export const generateEnvFile = (app: App) => {
envFile += `APP_DOMAIN=${app.domain}\n`;
envFile += 'APP_PROTOCOL=https\n';
} else {
envFile += `APP_DOMAIN=${config.INTERNAL_IP}:${configFile.port}\n`;
envFile += `APP_DOMAIN=${internalIp}:${configFile.port}\n`;
}
writeFile(`/app-data/${app.id}/app.env`, envFile);
@ -119,11 +121,11 @@ export const generateEnvFile = (app: App) => {
export const getAvailableApps = async (): Promise<string[]> => {
const apps: string[] = [];
const appsDir = readdirSync(`/repos/${config.APPS_REPO_ID}/apps`);
const appsDir = readdirSync(`/repos/${appsRepoId}/apps`);
appsDir.forEach((app) => {
if (fileExists(`/repos/${config.APPS_REPO_ID}/apps/${app}/config.json`)) {
const configFile: AppInfo = readJsonFile(`/repos/${config.APPS_REPO_ID}/apps/${app}/config.json`);
if (fileExists(`/repos/${appsRepoId}/apps/${app}/config.json`)) {
const configFile: AppInfo = readJsonFile(`/repos/${appsRepoId}/apps/${app}/config.json`);
if (configFile.available) {
apps.push(app);
@ -136,8 +138,6 @@ export const getAvailableApps = async (): Promise<string[]> => {
export const getAppInfo = (id: string, status?: AppStatusEnum): AppInfo | null => {
try {
const repoId = config.APPS_REPO_ID;
// Check if app is installed
const installed = typeof status !== 'undefined' && status !== AppStatusEnum.MISSING;
@ -145,9 +145,9 @@ export const getAppInfo = (id: string, status?: AppStatusEnum): AppInfo | null =
const configFile: AppInfo = readJsonFile(`/apps/${id}/config.json`);
configFile.description = readFile(`/apps/${id}/metadata/description.md`).toString();
return configFile;
} else if (fileExists(`/repos/${repoId}/apps/${id}/config.json`)) {
const configFile: AppInfo = readJsonFile(`/repos/${repoId}/apps/${id}/config.json`);
configFile.description = readFile(`/repos/${repoId}/apps/${id}/metadata/description.md`);
} else if (fileExists(`/repos/${appsRepoId}/apps/${id}/config.json`)) {
const configFile: AppInfo = readJsonFile(`/repos/${appsRepoId}/apps/${id}/config.json`);
configFile.description = readFile(`/repos/${appsRepoId}/apps/${id}/metadata/description.md`);
if (configFile.available) {
return configFile;
@ -164,13 +164,13 @@ export const getAppInfo = (id: string, status?: AppStatusEnum): AppInfo | null =
export const getUpdateInfo = async (id: string) => {
const app = await App.findOne({ where: { id } });
const doesFileExist = fileExists(`/repos/${config.APPS_REPO_ID}/apps/${id}`);
const doesFileExist = fileExists(`/repos/${appsRepoId}/apps/${id}`);
if (!app || !doesFileExist) {
return null;
}
const repoConfig: AppInfo = readJsonFile(`/repos/${config.APPS_REPO_ID}/apps/${id}/config.json`);
const repoConfig: AppInfo = readJsonFile(`/repos/${appsRepoId}/apps/${id}/config.json`);
return {
current: app.version,

View file

@ -4,8 +4,8 @@ import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps,
import { AppInfo, AppStatusEnum, ListAppsResonse } from './apps.types';
import App from './app.entity';
import logger from '../../config/logger/logger';
import config from '../../config';
import { Not } from 'typeorm';
import { getConfig } from '../../core/config/TipiConfig';
const sortApps = (a: AppInfo, b: AppInfo) => a.name.localeCompare(b.name);
@ -124,7 +124,7 @@ const listApps = async (): Promise<ListAppsResonse> => {
const apps: AppInfo[] = folders
.map((app) => {
try {
return readJsonFile(`/repos/${config.APPS_REPO_ID}/apps/${app}/config.json`);
return readJsonFile(`/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
} catch (e) {
return null;
}
@ -132,7 +132,7 @@ const listApps = async (): Promise<ListAppsResonse> => {
.filter(Boolean);
apps.forEach((app) => {
app.description = readFile(`/repos/${config.APPS_REPO_ID}/apps/${app.id}/metadata/description.md`);
app.description = readFile(`/repos/${getConfig().appsRepoId}/apps/${app.id}/metadata/description.md`);
});
return { apps: apps.sort(sortApps), total: apps.length };

View file

@ -1,7 +1,7 @@
import childProcess from 'child_process';
import config from '../../../config';
import { getAbsolutePath, readJsonFile, readFile, readdirSync, fileExists, writeFile, createFolder, deleteFolder, runScript, getSeed, ensureAppFolder } from '../fs.helpers';
import fs from 'fs-extra';
import { getConfig } from '../../../core/config/TipiConfig';
jest.mock('fs-extra');
@ -12,7 +12,7 @@ beforeEach(() => {
describe('Test: getAbsolutePath', () => {
it('should return the absolute path', () => {
expect(getAbsolutePath('/test')).toBe(`${config.ROOT_FOLDER}/test`);
expect(getAbsolutePath('/test')).toBe(`${getConfig().rootFolder}/test`);
});
});
@ -21,7 +21,7 @@ describe('Test: readJsonFile', () => {
// Arrange
const rawFile = '{"test": "test"}';
const mockFiles = {
[`${config.ROOT_FOLDER}/test-file.json`]: rawFile,
[`${getConfig().rootFolder}/test-file.json`]: rawFile,
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
@ -42,7 +42,7 @@ describe('Test: readFile', () => {
it('should return the file', () => {
const rawFile = 'test';
const mockFiles = {
[`${config.ROOT_FOLDER}/test-file.txt`]: rawFile,
[`${getConfig().rootFolder}/test-file.txt`]: rawFile,
};
// @ts-ignore
@ -59,7 +59,7 @@ describe('Test: readFile', () => {
describe('Test: readdirSync', () => {
it('should return the files', () => {
const mockFiles = {
[`${config.ROOT_FOLDER}/test/test-file.txt`]: 'test',
[`${getConfig().rootFolder}/test/test-file.txt`]: 'test',
};
// @ts-ignore
@ -76,7 +76,7 @@ describe('Test: readdirSync', () => {
describe('Test: fileExists', () => {
it('should return true if the file exists', () => {
const mockFiles = {
[`${config.ROOT_FOLDER}/test-file.txt`]: 'test',
[`${getConfig().rootFolder}/test-file.txt`]: 'test',
};
// @ts-ignore
@ -96,7 +96,7 @@ describe('Test: writeFile', () => {
writeFile('/test-file.txt', 'test');
expect(spy).toHaveBeenCalledWith(`${config.ROOT_FOLDER}/test-file.txt`, 'test');
expect(spy).toHaveBeenCalledWith(`${getConfig().rootFolder}/test-file.txt`, 'test');
});
});
@ -106,7 +106,7 @@ describe('Test: createFolder', () => {
createFolder('/test');
expect(spy).toHaveBeenCalledWith(`${config.ROOT_FOLDER}/test`);
expect(spy).toHaveBeenCalledWith(`${getConfig().rootFolder}/test`);
});
});
@ -116,7 +116,7 @@ describe('Test: deleteFolder', () => {
deleteFolder('/test');
expect(spy).toHaveBeenCalledWith(`${config.ROOT_FOLDER}/test`, { recursive: true });
expect(spy).toHaveBeenCalledWith(`${getConfig().rootFolder}/test`, { recursive: true });
});
});
@ -127,14 +127,14 @@ describe('Test: runScript', () => {
runScript('/test', [], callback);
expect(spy).toHaveBeenCalledWith(`${config.ROOT_FOLDER}/test`, [], {}, callback);
expect(spy).toHaveBeenCalledWith(`${getConfig().rootFolder}/test`, [], {}, callback);
});
});
describe('Test: getSeed', () => {
it('should return the seed', () => {
const mockFiles = {
[`${config.ROOT_FOLDER}/state/seed`]: 'test',
[`${getConfig().rootFolder}/state/seed`]: 'test',
};
// @ts-ignore
@ -147,7 +147,7 @@ describe('Test: getSeed', () => {
describe('Test: ensureAppFolder', () => {
beforeEach(() => {
const mockFiles = {
[`${config.ROOT_FOLDER}/repos/${config.APPS_REPO_ID}/apps/test`]: ['test.yml'],
[`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
@ -158,15 +158,15 @@ describe('Test: ensureAppFolder', () => {
ensureAppFolder('test');
// Assert
const files = fs.readdirSync(`${config.ROOT_FOLDER}/apps/test`);
const files = fs.readdirSync(`${getConfig().rootFolder}/apps/test`);
expect(files).toEqual(['test.yml']);
});
it('should not copy the folder if it already exists', () => {
const mockFiles = {
[`${config.ROOT_FOLDER}/repos/${config.APPS_REPO_ID}/apps/test`]: ['test.yml'],
[`${config.ROOT_FOLDER}/apps/test`]: ['docker-compose.yml'],
[`${config.ROOT_FOLDER}/apps/test/docker-compose.yml`]: 'test',
[`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
[`${getConfig().rootFolder}/apps/test`]: ['docker-compose.yml'],
[`${getConfig().rootFolder}/apps/test/docker-compose.yml`]: 'test',
};
// @ts-ignore
@ -176,15 +176,15 @@ describe('Test: ensureAppFolder', () => {
ensureAppFolder('test');
// Assert
const files = fs.readdirSync(`${config.ROOT_FOLDER}/apps/test`);
const files = fs.readdirSync(`${getConfig().rootFolder}/apps/test`);
expect(files).toEqual(['docker-compose.yml']);
});
it('Should overwrite the folder if clean up is true', () => {
const mockFiles = {
[`${config.ROOT_FOLDER}/repos/${config.APPS_REPO_ID}/apps/test`]: ['test.yml'],
[`${config.ROOT_FOLDER}/apps/test`]: ['docker-compose.yml'],
[`${config.ROOT_FOLDER}/apps/test/docker-compose.yml`]: 'test',
[`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
[`${getConfig().rootFolder}/apps/test`]: ['docker-compose.yml'],
[`${getConfig().rootFolder}/apps/test/docker-compose.yml`]: 'test',
};
// @ts-ignore
@ -194,7 +194,7 @@ describe('Test: ensureAppFolder', () => {
ensureAppFolder('test', true);
// Assert
const files = fs.readdirSync(`${config.ROOT_FOLDER}/apps/test`);
const files = fs.readdirSync(`${getConfig().rootFolder}/apps/test`);
expect(files).toEqual(['test.yml']);
});
});

View file

@ -1,8 +1,8 @@
import fs from 'fs-extra';
import childProcess from 'child_process';
import config from '../../config';
import { getConfig } from '../../core/config/TipiConfig';
export const getAbsolutePath = (path: string) => `${config.ROOT_FOLDER}${path}`;
export const getAbsolutePath = (path: string) => `${getConfig().rootFolder}${path}`;
export const readJsonFile = (path: string): any => {
try {
@ -54,6 +54,6 @@ export const ensureAppFolder = (appName: string, cleanup = false) => {
if (!fileExists(`/apps/${appName}/docker-compose.yml`)) {
if (fileExists(`/apps/${appName}`)) deleteFolder(`/apps/${appName}`);
// Copy from apps repo
fs.copySync(getAbsolutePath(`/repos/${config.APPS_REPO_ID}/apps/${appName}`), getAbsolutePath(`/apps/${appName}`));
fs.copySync(getAbsolutePath(`/repos/${getConfig().appsRepoId}/apps/${appName}`), getAbsolutePath(`/apps/${appName}`));
}
};

View file

@ -1,6 +1,6 @@
import axios from 'axios';
import config from '../../config';
import TipiCache from '../../config/TipiCache';
import { getConfig } from '../../core/config/TipiConfig';
import { readJsonFile } from '../fs/fs.helpers';
type SystemInfo = {
@ -38,9 +38,9 @@ const getVersion = async (): Promise<{ current: string; latest?: string }> => {
TipiCache.set('latestVersion', version?.replace('v', ''));
return { current: config.VERSION, latest: version?.replace('v', '') };
return { current: getConfig().version, latest: version?.replace('v', '') };
} catch (e) {
return { current: config.VERSION, latest: undefined };
return { current: getConfig().version, latest: undefined };
}
};

View file

@ -16,9 +16,8 @@ import { runUpdates } from './core/updates/run';
import recover from './core/updates/recover-migrations';
import { cloneRepo, updateRepo } from './helpers/repo-helpers';
import startJobs from './core/jobs/jobs';
import { getConfig } from './core/config/TipiConfig';
const { clientUrls, rootFolder, appsRepoId, appsRepoUrl } = getConfig();
import { applyJsonConfig, getConfig } from './core/config/TipiConfig';
import { ZodError } from 'zod';
let corsOptions = {
credentials: true,
@ -29,7 +28,7 @@ let corsOptions = {
// disallow requests with no origin
if (!origin) return callback(new Error('Not allowed by CORS'), false);
if (clientUrls.includes(origin)) {
if (getConfig().clientUrls.includes(origin)) {
return callback(null, true);
}
@ -38,12 +37,27 @@ let corsOptions = {
},
};
const applyCustomConfig = () => {
try {
applyJsonConfig();
} catch (e) {
logger.error('Error applying settings.json config');
if (e instanceof ZodError) {
Object.keys(e.flatten().fieldErrors).forEach((key) => {
logger.error(`Error in field ${key}`);
});
}
}
};
const main = async () => {
try {
applyCustomConfig();
const app = express();
const port = 3001;
app.use(express.static(`${rootFolder}/repos/${appsRepoId}`));
app.use(express.static(`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}`));
app.use(cors(corsOptions));
app.use(getSessionMiddleware());
@ -77,15 +91,15 @@ const main = async () => {
await runUpdates();
httpServer.listen(port, async () => {
await cloneRepo(appsRepoUrl);
await updateRepo(appsRepoId);
await cloneRepo(getConfig().appsRepoUrl);
await updateRepo(getConfig().appsRepoUrl);
startJobs();
// Start apps
appsService.startAllApps();
console.info(`Server running on port ${port} 🚀 Production => ${__prod__}`);
});
} catch (error) {
console.log(error);
console.error(error);
logger.error(error);
}
};

View file

@ -4,46 +4,28 @@
set -euo pipefail
# use greadlink instead of readlink on osx
if [[ "$(uname)" == "Darwin" ]]; then
rdlk=greadlink
else
rdlk=readlink
cd /runtipi || echo ""
# Ensure PWD ends with /runtipi
if [[ "${PWD##*/}" != "runtipi" ]]; then
echo "Please run this script from the runtipi directory"
exit 1
fi
ROOT_FOLDER="$($rdlk -f $(dirname "${BASH_SOURCE[0]}")/..)"
REPO_ID="$(echo -n "https://github.com/meienberger/runtipi-appstore" | sha256sum | awk '{print $1}')"
STATE_FOLDER="${ROOT_FOLDER}/state"
# Root folder in container is /runtipi
ROOT_FOLDER="${PWD}"
show_help() {
cat <<EOF
app 0.0.1
ENV_FILE="${ROOT_FOLDER}/.env"
CLI for managing Tipi apps
Usage: app <command> <app> [<arguments>]
Commands:
install Pulls down images for an app and starts it
uninstall Removes images and destroys all data for an app
stop Stops an installed app
start Starts an installed app
compose Passes all arguments to Docker Compose
ls-installed Lists installed apps
EOF
}
# Root folder in host system
ROOT_FOLDER_HOST=$(grep -v '^#' "${ENV_FILE}" | xargs -n 1 | grep ROOT_FOLDER_HOST | cut -d '=' -f2)
REPO_ID=$(grep -v '^#' "${ENV_FILE}" | xargs -n 1 | grep APPS_REPO_ID | cut -d '=' -f2)
# Get field from json file
function get_json_field() {
local json_file="$1"
local field="$2"
echo $(jq -r ".${field}" "${json_file}")
}
list_installed_apps() {
str=$(get_json_field ${STATE_FOLDER}/apps.json installed)
echo $str
jq -r ".${field}" "${json_file}"
}
if [ -z ${1+x} ]; then
@ -52,31 +34,11 @@ else
command="$1"
fi
# Lists installed apps
if [[ "$command" = "ls-installed" ]]; then
list_installed_apps
exit
fi
if [ -z ${2+x} ]; then
show_help
exit 1
else
app="$2"
root_folder_host="${3:-$ROOT_FOLDER}"
repo_id="${4:-$REPO_ID}"
if [[ -z "${repo_id}" ]]; then
echo "Error: Repo id not provided"
exit 1
fi
if [[ -z "${root_folder_host}" ]]; then
echo "Error: Root folder not provided"
exit 1
fi
app_dir="${ROOT_FOLDER}/apps/${app}"
@ -84,7 +46,7 @@ else
# copy from repo
echo "Copying app from repo"
mkdir -p "${app_dir}"
cp -r "${ROOT_FOLDER}/repos/${repo_id}/apps/${app}"/* "${app_dir}"
cp -r "${ROOT_FOLDER}/repos/${REPO_ID}/apps/${app}"/* "${app_dir}"
fi
app_data_dir="${ROOT_FOLDER}/app-data/${app}"
@ -113,7 +75,6 @@ compose() {
fi
# App data folder
local env_file="${ROOT_FOLDER}/.env"
local app_compose_file="${app_dir}/docker-compose.yml"
# Pick arm architecture if running on arm and if the app has a docker-compose.arm.yml file
@ -121,19 +82,14 @@ compose() {
app_compose_file="${app_dir}/docker-compose.arm.yml"
fi
local common_compose_file="${ROOT_FOLDER}/repos/${repo_id}/apps/docker-compose.common.yml"
local common_compose_file="${ROOT_FOLDER}/repos/${REPO_ID}/apps/docker-compose.common.yml"
# Vars to use in compose file
export APP_DATA_DIR="${root_folder_host}/app-data/${app}"
export APP_DIR="${app_dir}"
export ROOT_FOLDER_HOST="${root_folder_host}"
export ROOT_FOLDER="${ROOT_FOLDER}"
# Docker Compose does not support multiple env files
# --env-file "${env_file}" \
export APP_DATA_DIR="${ROOT_FOLDER_HOST}/app-data/${app}"
export ROOT_FOLDER_HOST="${ROOT_FOLDER_HOST}"
docker compose \
--env-file "${ROOT_FOLDER}/app-data/${app}/app.env" \
--env-file "${app_data_dir}/app.env" \
--project-name "${app}" \
--file "${app_compose_file}" \
--file "${common_compose_file}" \
@ -189,7 +145,7 @@ if [[ "$command" = "update" ]]; then
fi
# Copy app from repo
cp -r "${ROOT_FOLDER}/repos/${repo_id}/apps/${app}" "${app_dir}"
cp -r "${ROOT_FOLDER}/repos/${REPO_ID}/apps/${app}" "${app_dir}"
compose "${app}" pull
exit
@ -215,7 +171,4 @@ if [[ "$command" = "compose" ]]; then
exit
fi
# If we get here it means no valid command was supplied
# Show help and exit
show_help
exit 1

View file

@ -1,16 +1,4 @@
#!/usr/bin/env bash
ROOT_FOLDER="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")"/..)"
echo
echo "======================================"
if [[ -f "${ROOT_FOLDER}/state/configured" ]]; then
echo "=========== RECONFIGURING ============"
else
echo "============ CONFIGURING ============="
fi
echo "=============== TIPI ================="
echo "======================================"
echo
function install_docker() {
local os="${1}"

View file

@ -1,13 +1,15 @@
#!/usr/bin/env bash
# Don't break if command fails
# use greadlink instead of readlink on osx
if [[ "$(uname)" == "Darwin" ]]; then
rdlk=greadlink
else
rdlk=readlink
cd /runtipi || echo ""
# Ensure PWD ends with /runtipi
if [[ "${PWD##*/}" != "runtipi" ]]; then
echo ${PWD}
echo "Please run this script from the runtipi directory"
exit 1
fi
ROOT_FOLDER="$($rdlk -f $(dirname "${BASH_SOURCE[0]}")/..)"
ROOT_FOLDER="${PWD}"
show_help() {
cat <<EOF

View file

@ -106,7 +106,7 @@ if [[ "${NGINX_PORT}" != "80" ]] && [[ "${DOMAIN}" != "tipi.localhost" ]]; then
exit 1
fi
ROOT_FOLDER="$($readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
ROOT_FOLDER="${PWD}"
STATE_FOLDER="${ROOT_FOLDER}/state"
SED_ROOT_FOLDER="$(echo $ROOT_FOLDER | sed 's/\//\\\//g')"
@ -187,6 +187,31 @@ JWT_SECRET=$(derive_entropy "jwt")
POSTGRES_PASSWORD=$(derive_entropy "postgres")
TIPI_VERSION=$(get_json_field "${ROOT_FOLDER}/package.json" version)
# Override vars with values from settings.json
if [[ -f "${STATE_FOLDER}/settings.json" ]]; then
# If dnsIp is set in settings.json, use it
if [[ "$(get_json_field "${STATE_FOLDER}/settings.json" dnsIp)" != "null" ]]; then
DNS_IP=$(get_json_field "${STATE_FOLDER}/settings.json" dnsIp)
fi
# If domain is set in settings.json, use it
if [[ "$(get_json_field "${STATE_FOLDER}/settings.json" domain)" != "null" ]]; then
DOMAIN=$(get_json_field "${STATE_FOLDER}/settings.json" domain)
fi
# If appsRepoUrl is set in settings.json, use it
if [[ "$(get_json_field "${STATE_FOLDER}/settings.json" appsRepoUrl)" != "null" ]]; then
APPS_REPOSITORY_ESCAPED="$(echo ${APPS_REPOSITORY} | sed 's/\//\\\//g')"
fi
# If appsRepoId is set in settings.json, use it
if [[ "$(get_json_field "${STATE_FOLDER}/settings.json" appsRepoId)" != "null" ]]; then
REPO_ID=$(get_json_field "${STATE_FOLDER}/settings.json" appsRepoId)
fi
fi
echo "Creating .env file with the following values:"
echo " DOMAIN=${DOMAIN}"
echo " INTERNAL_IP=${INTERNAL_IP}"

View file

@ -1,13 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
# use greadlink instead of readlink on osx
if [[ "$(uname)" == "Darwin" ]]; then
readlink=greadlink
else
readlink=readlink
fi
if [[ $UID != 0 ]]; then
echo "Tipi must be stopped as root"
echo "Please re-run this script as"
@ -15,10 +8,13 @@ if [[ $UID != 0 ]]; then
exit 1
fi
ROOT_FOLDER="$($readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
STATE_FOLDER="${ROOT_FOLDER}/state"
# Ensure PWD ends with /runtipi
if [[ $(basename $(pwd)) != "runtipi" ]] || [[ ! -f "${BASH_SOURCE[0]}" ]]; then
echo "Please run this script from the runtipi directory"
exit 1
fi
cd "$ROOT_FOLDER"
ROOT_FOLDER="${PWD}"
export DOCKER_CLIENT_TIMEOUT=240
export COMPOSE_HTTP_TIMEOUT=240

View file

@ -1,25 +1,32 @@
#!/usr/bin/env bash
set -e # Exit immediately if a command exits with a non-zero status.
ROOT_FOLDER="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
cd /runtipi || echo ""
# Ensure PWD ends with /runtipi
if [[ "${PWD##*/}" != "runtipi" ]]; then
echo "Please run this script from the runtipi directory"
exit 1
fi
ROOT_FOLDER="${PWD}"
STATE_FOLDER="${ROOT_FOLDER}/state"
# Available disk space
TOTAL_DISK_SPACE_BYTES=$(df -P -B 1 / | tail -n 1 | awk '{print $2}')
AVAILABLE_DISK_SPACE_BYTES=$(df -P -B 1 / | tail -n 1 | awk '{print $4}')
USED_DISK_SPACE_BYTES=$(($TOTAL_DISK_SPACE_BYTES - $AVAILABLE_DISK_SPACE_BYTES))
USED_DISK_SPACE_BYTES=$((TOTAL_DISK_SPACE_BYTES - AVAILABLE_DISK_SPACE_BYTES))
# CPU info
CPU_LOAD_PERCENTAGE=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}')
# Memory info
MEM_TOTAL_BYTES=$(($(cat /proc/meminfo | grep MemTotal | awk '{print $2}') * 1024))
MEM_AVAILABLE_BYTES=$(($(cat /proc/meminfo | grep MemAvailable | awk '{print $2}') * 1024))
MEM_USED_BYTES=$(($MEM_TOTAL_BYTES - $MEM_AVAILABLE_BYTES))
MEM_TOTAL_BYTES=$(($(grep </proc/meminfo MemTotal | awk '{print $2}') * 1024))
MEM_AVAILABLE_BYTES=$(($(grep </proc/meminfo MemAvailable | awk '{print $2}') * 1024))
MEM_USED_BYTES=$((MEM_TOTAL_BYTES - MEM_AVAILABLE_BYTES))
# Create temporary json file
TEMP_JSON_FILE=$(mktemp)
echo '{ "cpu": { "load": '"${CPU_LOAD_PERCENTAGE}"' }, "memory": { "total": '"${MEM_TOTAL_BYTES}"' , "used": '"${MEM_USED_BYTES}"', "available": '"${MEM_AVAILABLE_BYTES}"' }, "disk": { "total": '"${TOTAL_DISK_SPACE_BYTES}"' , "used": '"${USED_DISK_SPACE_BYTES}"', "available": '"${AVAILABLE_DISK_SPACE_BYTES}"' } }' >"${TEMP_JSON_FILE}"
# Write to state file
echo "$(cat "${TEMP_JSON_FILE}")" >"${STATE_FOLDER}/system-info.json"
cat "${TEMP_JSON_FILE}" >"${STATE_FOLDER}/system-info.json"

35
scripts/utils.sh Executable file
View file

@ -0,0 +1,35 @@
#!/usr/bin/env bash
cd /runtipi || echo ""
# Ensure PWD ends with /runtipi
if [[ "${PWD##*/}" != "runtipi" ]]; then
echo "Please run this script from the runtipi directory"
exit 1
fi
if [ -z ${1+x} ]; then
command=""
else
command="$1"
fi
# Restart Tipi
if [[ "$command" = "restart" ]]; then
echo "Restarting Tipi..."
scripts/stop.sh
scripts/start.sh
exit
fi
# Update Tipi
if [[ "$command" = "update" ]]; then
scripts/stop.sh
git pull origin master
scripts/start.sh
exit
fi