mirror of
https://github.com/unraid/api.git
synced 2025-12-20 08:09:42 -06:00
fix: auto-uninstallation of connect api plugin (#1791)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Plugin configuration now lives in a single API configuration object for consistent handling. * Connection plugin wiring simplified so the connect plugin is always provided without runtime fallbacks. * **Chores** * Startup now automatically removes stale connect-plugin entries from saved config when the plugin is absent, improving startup reliability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
12
api/src/connect-plugin-cleanup.ts
Normal file
12
api/src/connect-plugin-cleanup.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
/**
|
||||
* Local filesystem and env checks stay synchronous so we can branch at module load.
|
||||
* @returns True if the Connect Unraid plugin is installed, false otherwise.
|
||||
*/
|
||||
export const isConnectPluginInstalled = () => {
|
||||
if (process.env.SKIP_CONNECT_PLUGIN_CHECK === 'true') {
|
||||
return true;
|
||||
}
|
||||
return existsSync('/boot/config/plugins/dynamix.unraid.net.plg');
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import '@app/dotenv.js';
|
||||
|
||||
import { type NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { unlinkSync } from 'fs';
|
||||
import { mkdir } from 'fs/promises';
|
||||
import { mkdir, readFile } from 'fs/promises';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ApiConfig } from '@unraid/shared/services/api-config.js';
|
||||
import { ConfigFilePersister } from '@unraid/shared/services/config-file.js';
|
||||
import { csvStringToArray } from '@unraid/shared/util/data.js';
|
||||
|
||||
import { isConnectPluginInstalled } from '@app/connect-plugin-cleanup.js';
|
||||
import { API_VERSION, PATHS_CONFIG_MODULES } from '@app/environment.js';
|
||||
|
||||
export { type ApiConfig };
|
||||
@@ -29,6 +30,13 @@ export const loadApiConfig = async () => {
|
||||
const apiHandler = new ApiConfigPersistence(new ConfigService()).getFileHandler();
|
||||
|
||||
const diskConfig: Partial<ApiConfig> = await apiHandler.loadConfig();
|
||||
// Hack: cleanup stale connect plugin entry if necessary
|
||||
if (!isConnectPluginInstalled()) {
|
||||
diskConfig.plugins = diskConfig.plugins?.filter(
|
||||
(plugin) => plugin !== 'unraid-api-plugin-connect'
|
||||
);
|
||||
await apiHandler.writeConfigFile(diskConfig as ApiConfig);
|
||||
}
|
||||
|
||||
return {
|
||||
...defaultConfig,
|
||||
|
||||
@@ -21,9 +21,19 @@ describe('PluginManagementService', () => {
|
||||
if (key === 'api.plugins') {
|
||||
return configStore ?? defaultValue ?? [];
|
||||
}
|
||||
if (key === 'api') {
|
||||
return { plugins: configStore ?? defaultValue ?? [] };
|
||||
}
|
||||
return defaultValue;
|
||||
}),
|
||||
set: vi.fn((key: string, value: unknown) => {
|
||||
if (key === 'api' && typeof value === 'object' && value !== null) {
|
||||
// @ts-expect-error - value is an object
|
||||
if (Array.isArray(value.plugins)) {
|
||||
// @ts-expect-error - value is an object
|
||||
configStore = [...value.plugins];
|
||||
}
|
||||
}
|
||||
if (key === 'api.plugins' && Array.isArray(value)) {
|
||||
configStore = [...value];
|
||||
}
|
||||
|
||||
@@ -56,8 +56,7 @@ export class PluginManagementService {
|
||||
}
|
||||
pluginSet.add(plugin);
|
||||
});
|
||||
// @ts-expect-error - This is a valid config key
|
||||
this.configService.set<string[]>('api.plugins', Array.from(pluginSet));
|
||||
this.updatePluginsConfig(Array.from(pluginSet));
|
||||
return added;
|
||||
}
|
||||
|
||||
@@ -71,11 +70,15 @@ export class PluginManagementService {
|
||||
const pluginSet = new Set(this.plugins);
|
||||
const removed = plugins.filter((plugin) => pluginSet.delete(plugin));
|
||||
const pluginsArray = Array.from(pluginSet);
|
||||
// @ts-expect-error - This is a valid config key
|
||||
this.configService.set('api.plugins', pluginsArray);
|
||||
this.updatePluginsConfig(pluginsArray);
|
||||
return removed;
|
||||
}
|
||||
|
||||
private updatePluginsConfig(plugins: string[]) {
|
||||
const apiConfig = this.configService.get<ApiConfig>('api');
|
||||
this.configService.set('api', { ...apiConfig, plugins });
|
||||
}
|
||||
|
||||
/**
|
||||
* Install bundle / unbundled plugins using npm or direct with the config.
|
||||
*
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { Inject, Logger, Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
import { execa } from 'execa';
|
||||
|
||||
import { ConnectConfigPersister } from './config/config.persistence.js';
|
||||
import { configFeature } from './config/connect.config.js';
|
||||
@@ -30,64 +27,4 @@ class ConnectPluginModule {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback module keeps the export shape intact but only warns operators.
|
||||
* This makes `ApiModule` safe to import even when the plugin is absent.
|
||||
*/
|
||||
@Module({})
|
||||
export class DisabledConnectPluginModule {
|
||||
logger = new Logger(DisabledConnectPluginModule.name);
|
||||
async onModuleInit() {
|
||||
const removalCommand = 'unraid-api plugins remove -b unraid-api-plugin-connect';
|
||||
|
||||
this.logger.warn(
|
||||
'Connect plugin is not installed, but is listed as an API plugin. Attempting `%s` automatically.',
|
||||
removalCommand
|
||||
);
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execa('unraid-api', [
|
||||
'plugins',
|
||||
'remove',
|
||||
'-b',
|
||||
'unraid-api-plugin-connect',
|
||||
]);
|
||||
|
||||
if (stdout?.trim()) {
|
||||
this.logger.debug(stdout.trim());
|
||||
}
|
||||
|
||||
if (stderr?.trim()) {
|
||||
this.logger.debug(stderr.trim());
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
'Successfully completed `%s` to prune the stale connect plugin entry.',
|
||||
removalCommand
|
||||
);
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown error while removing stale connect plugin entry.';
|
||||
this.logger.error('Failed to run `%s`: %s', removalCommand, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Local filesystem and env checks stay synchronous so we can branch at module load.
|
||||
*/
|
||||
const isConnectPluginInstalled = () => {
|
||||
if (process.env.SKIP_CONNECT_PLUGIN_CHECK === 'true') {
|
||||
return true;
|
||||
}
|
||||
return existsSync('/boot/config/plugins/dynamix.unraid.net.plg');
|
||||
};
|
||||
|
||||
/**
|
||||
* Downstream code always imports `ApiModule`. We swap the implementation based on availability,
|
||||
* avoiding dynamic module plumbing while keeping the DI graph predictable.
|
||||
* Set `SKIP_CONNECT_PLUGIN_CHECK=true` in development to force the connected path.
|
||||
*/
|
||||
export const ApiModule = isConnectPluginInstalled() ? ConnectPluginModule : DisabledConnectPluginModule;
|
||||
export const ApiModule = ConnectPluginModule;
|
||||
|
||||
Reference in New Issue
Block a user