feat: hide apps which are not supported on the host architecture

This commit is contained in:
Nicolas Meienberger 2022-10-18 19:02:25 +02:00
parent 5091bc4f6b
commit 459842675a
7 changed files with 127 additions and 4 deletions

View file

@ -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,

View file

@ -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) {

View file

@ -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', () => {

View file

@ -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;
};

View file

@ -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 };
};
/**

View file

@ -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()

View file

@ -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
### --------------------------------