feat: hide apps which are not supported on the host architecture
This commit is contained in:
parent
b29d6cc234
commit
b9667f29f1
7 changed files with 127 additions and 4 deletions
|
@ -2,6 +2,7 @@ import { z } from 'zod';
|
|||
import * as dotenv from 'dotenv';
|
||||
import fs from 'fs-extra';
|
||||
import { readJsonFile } from '../../modules/fs/fs.helpers';
|
||||
import { AppSupportedArchitecturesEnum } from '../../modules/apps/apps.types';
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
dotenv.config({ path: '.env.dev' });
|
||||
|
@ -21,11 +22,13 @@ const {
|
|||
APPS_REPO_URL = '',
|
||||
DOMAIN = '',
|
||||
STORAGE_PATH = '/runtipi',
|
||||
ARCHITECTURE = 'amd64',
|
||||
} = process.env;
|
||||
|
||||
const configSchema = z.object({
|
||||
NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]),
|
||||
status: z.union([z.literal('RUNNING'), z.literal('UPDATING'), z.literal('RESTARTING')]),
|
||||
architecture: z.nativeEnum(AppSupportedArchitecturesEnum),
|
||||
logs: z.object({
|
||||
LOGS_FOLDER: z.string(),
|
||||
LOGS_APP: z.string(),
|
||||
|
@ -56,6 +59,7 @@ class Config {
|
|||
LOGS_ERROR,
|
||||
},
|
||||
NODE_ENV: NODE_ENV as z.infer<typeof configSchema>['NODE_ENV'],
|
||||
architecture: ARCHITECTURE as z.infer<typeof configSchema>['architecture'],
|
||||
rootFolder: '/runtipi',
|
||||
internalIp: INTERNAL_IP,
|
||||
version: TIPI_VERSION,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { faker } from '@faker-js/faker';
|
||||
import { AppCategoriesEnum, AppInfo, AppStatusEnum, FieldTypes } from '../apps.types';
|
||||
import { AppCategoriesEnum, AppInfo, AppStatusEnum, AppSupportedArchitecturesEnum, FieldTypes } from '../apps.types';
|
||||
import App from '../app.entity';
|
||||
|
||||
interface IProps {
|
||||
|
@ -10,10 +10,11 @@ interface IProps {
|
|||
exposed?: boolean;
|
||||
domain?: string;
|
||||
exposable?: boolean;
|
||||
supportedArchitectures?: AppSupportedArchitecturesEnum[];
|
||||
}
|
||||
|
||||
const createApp = async (props: IProps) => {
|
||||
const { installed = false, status = AppStatusEnum.RUNNING, requiredPort, randomField = false, exposed = false, domain = '', exposable = false } = props;
|
||||
const { installed = false, status = AppStatusEnum.RUNNING, requiredPort, randomField = false, exposed = false, domain = '', exposable = false, supportedArchitectures } = props;
|
||||
|
||||
const categories = Object.values(AppCategoriesEnum);
|
||||
|
||||
|
@ -29,6 +30,7 @@ const createApp = async (props: IProps) => {
|
|||
env_variable: 'TEST_FIELD',
|
||||
},
|
||||
],
|
||||
|
||||
name: faker.random.word(),
|
||||
description: faker.random.words(),
|
||||
tipi_version: faker.datatype.number({ min: 1, max: 10 }),
|
||||
|
@ -37,6 +39,7 @@ const createApp = async (props: IProps) => {
|
|||
source: faker.internet.url(),
|
||||
categories: [categories[faker.datatype.number({ min: 0, max: categories.length - 1 })]],
|
||||
exposable,
|
||||
supported_architectures: supportedArchitectures,
|
||||
};
|
||||
|
||||
if (randomField) {
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import AppsService from '../apps.service';
|
||||
import fs from 'fs-extra';
|
||||
import { AppInfo, AppStatusEnum } from '../apps.types';
|
||||
import { AppInfo, AppStatusEnum, AppSupportedArchitecturesEnum } from '../apps.types';
|
||||
import App from '../app.entity';
|
||||
import { createApp } from './apps.factory';
|
||||
import { setupConnection, teardownConnection } from '../../../test/connection';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { getEnvMap } from '../apps.helpers';
|
||||
import EventDispatcher, { eventDispatcher, EventTypes } from '../../../core/config/EventDispatcher';
|
||||
import { setConfig } from '../../../core/config/TipiConfig';
|
||||
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('child_process');
|
||||
|
@ -152,6 +153,38 @@ describe('Install app', () => {
|
|||
|
||||
await expect(AppsService.installApp(app3.appInfo.id, { TEST_FIELD: 'test' }, true, 'test.com')).rejects.toThrowError(`Domain test.com already in use by app ${app2.appInfo.id}`);
|
||||
});
|
||||
|
||||
it('Should throw if architecure is not supported', async () => {
|
||||
const { MockFiles, appInfo } = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM] });
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MockFiles);
|
||||
|
||||
await expect(AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' })).rejects.toThrowError(`App ${appInfo.id} is not supported on this architecture`);
|
||||
});
|
||||
|
||||
it('Can install if architecture is supported', async () => {
|
||||
setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
|
||||
const { MockFiles, appInfo } = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM, AppSupportedArchitecturesEnum.ARM64] });
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MockFiles);
|
||||
|
||||
await AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' });
|
||||
const app = await App.findOne({ where: { id: appInfo.id } });
|
||||
|
||||
expect(app).toBeDefined();
|
||||
});
|
||||
|
||||
it('Can install if no architecture is specified', async () => {
|
||||
setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
|
||||
const { MockFiles, appInfo } = await createApp({ supportedArchitectures: undefined });
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MockFiles);
|
||||
|
||||
await AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' });
|
||||
const app = await App.findOne({ where: { id: appInfo.id } });
|
||||
|
||||
expect(app).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Uninstall app', () => {
|
||||
|
@ -431,6 +464,50 @@ describe('List apps', () => {
|
|||
expect(apps[1].id).toBe(sortedApps[1].id);
|
||||
expect(apps[0].description).toBe('md desc');
|
||||
});
|
||||
|
||||
it('Should not list apps that have supportedArchitectures and are not supported', async () => {
|
||||
// Arrange
|
||||
setConfig('architecture', AppSupportedArchitecturesEnum.ARM64);
|
||||
const app3 = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM] });
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(Object.assign(app3.MockFiles));
|
||||
|
||||
// Act
|
||||
const { apps } = await AppsService.listApps();
|
||||
|
||||
// Assert
|
||||
expect(apps).toBeDefined();
|
||||
expect(apps.length).toBe(0);
|
||||
});
|
||||
|
||||
it('Should list apps that have supportedArchitectures and are supported', async () => {
|
||||
// Arrange
|
||||
setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
|
||||
const app3 = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM] });
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(Object.assign(app3.MockFiles));
|
||||
// Act
|
||||
const { apps } = await AppsService.listApps();
|
||||
|
||||
// Assert
|
||||
expect(apps).toBeDefined();
|
||||
expect(apps.length).toBe(1);
|
||||
});
|
||||
|
||||
it('Should list apps that have no supportedArchitectures specified', async () => {
|
||||
// Arrange
|
||||
setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
|
||||
const app3 = await createApp({});
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(Object.assign(app3.MockFiles));
|
||||
|
||||
// Act
|
||||
const { apps } = await AppsService.listApps();
|
||||
|
||||
// Assert
|
||||
expect(apps).toBeDefined();
|
||||
expect(apps.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Start all apps', () => {
|
||||
|
|
|
@ -26,6 +26,10 @@ export const checkAppRequirements = async (appName: string) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (configFile?.supported_architectures && !configFile.supported_architectures.includes(getConfig().architecture)) {
|
||||
throw new Error(`App ${appName} is not supported on this architecture`);
|
||||
}
|
||||
|
||||
return valid;
|
||||
};
|
||||
|
||||
|
|
|
@ -9,6 +9,18 @@ import { getConfig } from '../../core/config/TipiConfig';
|
|||
import { eventDispatcher, EventTypes } from '../../core/config/EventDispatcher';
|
||||
|
||||
const sortApps = (a: AppInfo, b: AppInfo) => a.name.localeCompare(b.name);
|
||||
const filterApp = (app: AppInfo): boolean => {
|
||||
if (!app.supported_architectures) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const arch = getConfig().architecture;
|
||||
return app.supported_architectures.includes(arch);
|
||||
};
|
||||
|
||||
const filterApps = (apps: AppInfo[]): AppInfo[] => {
|
||||
return apps.sort(sortApps).filter(filterApp);
|
||||
};
|
||||
|
||||
/**
|
||||
* Start all apps which had the status RUNNING in the database
|
||||
|
@ -159,7 +171,7 @@ const listApps = async (): Promise<ListAppsResonse> => {
|
|||
app.description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app.id}/metadata/description.md`);
|
||||
});
|
||||
|
||||
return { apps: apps.sort(sortApps), total: apps.length };
|
||||
return { apps: filterApps(apps), total: apps.length };
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -41,6 +41,12 @@ export enum AppStatusEnum {
|
|||
UPDATING = 'updating',
|
||||
}
|
||||
|
||||
export enum AppSupportedArchitecturesEnum {
|
||||
ARM = 'arm',
|
||||
ARM64 = 'arm64',
|
||||
AMD64 = 'amd64',
|
||||
}
|
||||
|
||||
registerEnumType(AppCategoriesEnum, {
|
||||
name: 'AppCategoriesEnum',
|
||||
});
|
||||
|
@ -49,6 +55,10 @@ registerEnumType(FieldTypes, {
|
|||
name: 'FieldTypesEnum',
|
||||
});
|
||||
|
||||
registerEnumType(AppSupportedArchitecturesEnum, {
|
||||
name: 'AppSupportedArchitecturesEnum',
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
class FormField {
|
||||
@Field(() => FieldTypes)
|
||||
|
@ -128,6 +138,9 @@ class AppInfo {
|
|||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
exposable?: boolean;
|
||||
|
||||
@Field(() => [AppSupportedArchitecturesEnum], { nullable: true })
|
||||
supported_architectures?: AppSupportedArchitecturesEnum[];
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
|
|
|
@ -61,6 +61,16 @@ INTERNAL_IP="$(ip addr show "${NETWORK_INTERFACE}" | grep "inet " | awk '{print
|
|||
|
||||
if [[ "$ARCHITECTURE" == "aarch64" ]]; then
|
||||
ARCHITECTURE="arm64"
|
||||
elif [[ "$ARCHITECTURE" == "armv7l" ]]; then
|
||||
ARCHITECTURE="arm"
|
||||
elif [[ "$ARCHITECTURE" == "x86_64" ]]; then
|
||||
ARCHITECTURE="amd64"
|
||||
fi
|
||||
|
||||
# If none of the above conditions are met, the architecture is not supported
|
||||
if [[ "$ARCHITECTURE" != "arm64" ]] && [[ "$ARCHITECTURE" != "arm" ]] && [[ "$ARCHITECTURE" != "amd64" ]]; then
|
||||
echo "Architecture not supported!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
### --------------------------------
|
||||
|
|
Loading…
Add table
Reference in a new issue