Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
d9c30ef
add CONFIG_MODULE_HOME env var to hold config files
pujitm Apr 10, 2025
87a9173
scaffold config registry & `ApiStateConfig`
pujitm Apr 10, 2025
1a6da27
provide CONFIG_MODULES_HOME to plugins
pujitm Apr 11, 2025
b1a86fa
add persistence to connect config
pujitm Apr 11, 2025
e20cbdf
rm @unraid/api-config from the api
pujitm Apr 11, 2025
53d2f94
load config on startup
pujitm Apr 14, 2025
bf40b7f
resolve pnpm lockfile
pujitm Apr 14, 2025
86cd4b4
re-connect plugin interface
pujitm Apr 14, 2025
5b76f18
rm `graphQLSchema` from plugin module validation
pujitm Apr 14, 2025
7e4aee6
us ts modules in unraid-api-plugin-connect
pujitm Apr 14, 2025
e0fe0f4
Merge branch 'main' into feat/config-module
pujitm Apr 17, 2025
a791d54
rm `PluginService::getSchemaExtension`
pujitm Apr 17, 2025
c7a7711
translate myservers.cfg to connect.json on init
pujitm Apr 17, 2025
904d06f
add cli command to add/rm plugins
pujitm Apr 17, 2025
db4f475
fix: api crash during init when docker is disabled
pujitm Apr 17, 2025
0607319
create generator for api plugins
pujitm Apr 18, 2025
416f6ea
move generator templates to separate files
pujitm Apr 18, 2025
84180e5
make template files valid typescript
pujitm Apr 18, 2025
b64d4ea
add type safety to template files in generator
pujitm Apr 18, 2025
97ccaed
make generator cli next steps dynamic
pujitm Apr 21, 2025
f292004
scope connect persistence changes to connect config feature
pujitm Apr 21, 2025
775688e
implement `OnModuleInit` in config persisters
pujitm Apr 21, 2025
38697d7
rm zod from plugins
pujitm Apr 21, 2025
6d7e861
refactor LogService to not be strict about what it accepts
pujitm Apr 21, 2025
acf1dbf
refactor: include stack trace in plugin.command errors
pujitm Apr 21, 2025
41d5ac5
make `persist` writeFile async in generated plugins
pujitm Apr 21, 2025
c90fa36
make `ConnectConfigPersister` sane
pujitm Apr 21, 2025
f0d4b27
add stack trace when config file doesn't exist
pujitm Apr 21, 2025
1ceb65d
improve plugin name validation in generator
pujitm Apr 21, 2025
90a3a16
replace custom ini parser with base parser from `ini` pkg
pujitm Apr 21, 2025
938bc6d
add `unraidVersion` config to generated package.json
pujitm Apr 21, 2025
c9d269f
improve package name validation in generator
pujitm Apr 21, 2025
5540616
change mkdir call in api startup to async
pujitm Apr 21, 2025
481b635
refactor plugin commands into a module
pujitm Apr 21, 2025
e78056a
add `persistIfChanged` helper
pujitm Apr 21, 2025
c6fcb00
add error handling when loading config in `persist`
pujitm Apr 21, 2025
910e161
fix returned `version` from `DependencyService.addPeerDependency`
pujitm Apr 21, 2025
5f87b7a
improve wip config helpers
pujitm Apr 21, 2025
758dd34
rm `ConfigRegistry`
pujitm Apr 21, 2025
fb07522
export ScheduledConfigPersister
pujitm Apr 21, 2025
d7817b5
clean up config.interface
pujitm Apr 22, 2025
c368e31
add nest lifecycle interfaces to `ScheduledConfigPersistence`
pujitm Apr 22, 2025
aab0a48
rename `CONFIG_MODULES_HOME` to `PATHS_CONFIG_MODULES`
pujitm Apr 22, 2025
7b08589
fix validation in config.persistence
pujitm Apr 22, 2025
7053070
add LogService and PM2Service to plugin cli module
pujitm Apr 22, 2025
fb2d03e
correct config.interface -> validate to async
pujitm Apr 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ PATHS_MY_SERVERS_FB=./dev/Unraid.net/fb_keepalive # My servers flashbackup timek
PATHS_KEYFILE_BASE=./dev/Unraid.net # Keyfile location
PATHS_MACHINE_ID=./dev/data/machine-id
PATHS_PARITY_CHECKS=./dev/states/parity-checks.log
PATHS_CONFIG_MODULES=./dev/configs
ENVIRONMENT="development"
NODE_ENV="development"
PORT="3001"
Expand All @@ -21,4 +22,4 @@ BYPASS_PERMISSION_CHECKS=false
BYPASS_CORS_CHECKS=true
CHOKIDAR_USEPOLLING=true
LOG_TRANSPORT=console
LOG_LEVEL=trace
LOG_LEVEL=trace
1 change: 1 addition & 0 deletions api/.env.production
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ ENVIRONMENT="production"
NODE_ENV="production"
PORT="/var/run/unraid-api.sock"
MOTHERSHIP_GRAPHQL_LINK="https://mothership.unraid.net/ws"
PATHS_CONFIG_MODULES="/boot/config/plugins/dynamix.my.servers/configs"
1 change: 1 addition & 0 deletions api/.env.staging
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ ENVIRONMENT="staging"
NODE_ENV="production"
PORT="/var/run/unraid-api.sock"
MOTHERSHIP_GRAPHQL_LINK="https://staging.mothership.unraid.net/ws"
PATHS_CONFIG_MODULES="/boot/config/plugins/dynamix.my.servers/configs"
3 changes: 2 additions & 1 deletion api/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ PATHS_MY_SERVERS_FB=./dev/Unraid.net/fb_keepalive # My servers flashbackup timek
PATHS_KEYFILE_BASE=./dev/Unraid.net # Keyfile location
PATHS_MACHINE_ID=./dev/data/machine-id
PATHS_PARITY_CHECKS=./dev/states/parity-checks.log
PATHS_CONFIG_MODULES=./dev/configs
PORT=5000
NODE_ENV="test"
NODE_ENV="test"
2 changes: 1 addition & 1 deletion api/dev/Unraid.net/myservers.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[api]
version="4.4.1"
version="4.6.6"
extraOrigins="https://google.com,https://test.com"
[local]
sandbox="yes"
Expand Down
23 changes: 23 additions & 0 deletions api/dev/configs/connect.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"demo": "2025-04-21T14:27:27.631Z",
"wanaccess": "yes",
"wanport": "8443",
"upnpEnabled": "no",
"apikey": "_______________________BIG_API_KEY_HERE_________________________",
"localApiKey": "_______________________LOCAL_API_KEY_HERE_________________________",
"email": "test@example.com",
"username": "zspearmint",
"avatar": "https://via.placeholder.com/200",
"regWizTime": "1611175408732_0951-1653-3509-FBA155FA23C0",
"accesstoken": "",
"idtoken": "",
"refreshtoken": "",
"dynamicRemoteAccessType": "DISABLED",
"ssoSubIds": "",
"version": "4.6.6",
"extraOrigins": [
"https://google.com",
"https://test.com"
],
"sandbox": "yes"
}
6 changes: 4 additions & 2 deletions api/generated-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1232,8 +1232,8 @@ type DockerNetwork {

type Docker implements Node {
id: ID!
containers(useCache: Boolean! = true): [DockerContainer!]!
networks: [DockerNetwork!]!
containers(skipCache: Boolean! = false): [DockerContainer!]!
networks(skipCache: Boolean! = false): [DockerNetwork!]!
}

type Flash implements Node {
Expand Down Expand Up @@ -1423,6 +1423,7 @@ type Query {
disks: [Disk!]!
disk(id: String!): Disk!
health: String!
getDemo: String!
}

type Mutation {
Expand Down Expand Up @@ -1459,6 +1460,7 @@ type Mutation {
setupRemoteAccess(input: SetupRemoteAccessInput!): Boolean!
setAdditionalAllowedOrigins(input: AllowedOriginInput!): [String!]!
enableDynamicRemoteAccess(input: EnableDynamicRemoteAccessInput!): Boolean!
setDemo: String!
}

input CreateApiKeyInput {
Expand Down
4 changes: 4 additions & 0 deletions api/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ default:

alias b := build
alias d := deploy

sync-env server:
rsync -avz --progress --stats -e ssh .env* root@{{server}}:/usr/local/unraid-api
ssh root@{{server}} 'cp /usr/local/unraid-api/.env.staging /usr/local/unraid-api/.env'
1 change: 1 addition & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@
"@types/ini": "^4.1.1",
"@types/ip": "^1.1.3",
"@types/lodash": "^4.17.13",
"@types/lodash-es": "^4.17.12",
"@types/mustache": "^4.2.5",
"@types/node": "^22.13.4",
"@types/pify": "^6.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ test('it creates a FLASH config with OPTIONAL values', () => {
// 2fa & t2fa should be ignored
basicConfig.remote['2Fa'] = 'yes';
basicConfig.local['2Fa'] = 'yes';
basicConfig.local.showT2Fa = 'yes';

basicConfig.api.extraOrigins = 'myextra.origins';
basicConfig.remote.upnpEnabled = 'yes';
Expand Down Expand Up @@ -120,7 +119,6 @@ test('it creates a MEMORY config with OPTIONAL values', () => {
// 2fa & t2fa should be ignored
basicConfig.remote['2Fa'] = 'yes';
basicConfig.local['2Fa'] = 'yes';
basicConfig.local.showT2Fa = 'yes';
basicConfig.api.extraOrigins = 'myextra.origins';
basicConfig.remote.upnpEnabled = 'yes';
basicConfig.connectionStatus.upnpStatus = 'Turned On';
Expand Down
54 changes: 32 additions & 22 deletions api/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,49 @@ import { fileURLToPath } from 'node:url';

import type { PackageJson, SetRequired } from 'type-fest';

import { fileExistsSync } from '@app/core/utils/files/file-exists.js';

/**
* Tries to get the package.json at the given location.
* @param location - The location of the package.json file, relative to the current file
* @returns The package.json object or undefined if unable to read
* Returns the absolute path to the given file.
* @param location - The location of the file, relative to the current file
* @returns The absolute path to the file
*/
function readPackageJson(location: string): PackageJson | undefined {
function getAbsolutePath(location: string): string {
try {
let packageJsonPath: string;
try {
const packageJsonUrl = import.meta.resolve(location);
packageJsonPath = fileURLToPath(packageJsonUrl);
} catch {
// Fallback (e.g. for local development): resolve the path relative to this module
packageJsonPath = fileURLToPath(new URL(location, import.meta.url));
}
const packageJsonRaw = readFileSync(packageJsonPath, 'utf-8');
return JSON.parse(packageJsonRaw) as PackageJson;
const fileUrl = import.meta.resolve(location);
return fileURLToPath(fileUrl);
} catch {
return undefined;
return fileURLToPath(new URL(location, import.meta.url));
}
}
/**
* Returns the path to the api's package.json file. Throws if unable to find.
* @param possiblePaths - The possible locations of the package.json file, relative to the current file
* @returns The absolute path to the package.json file
*/
export function getPackageJsonPath(possiblePaths = ['../package.json', '../../package.json']): string {
for (const location of possiblePaths) {
const packageJsonPath = getAbsolutePath(location);
if (fileExistsSync(packageJsonPath)) {
return packageJsonPath;
}
}
throw new Error(
`Could not find package.json in any of the expected locations: ${possiblePaths.join(', ')}`
);
}

/**
* Retrieves the Unraid API package.json. Throws if unable to find.
* Retrieves the Unraid API package.json. Throws if unable to find or parse.
* This should be considered a fatal error.
*
* @param pathOverride - The path to the package.json file. If not provided, the default path will be found & used.
* @returns The package.json object
*/
export const getPackageJson = () => {
const packageJson = readPackageJson('../package.json') || readPackageJson('../../package.json');
if (!packageJson) {
throw new Error('Could not find package.json in any of the expected locations');
}
return packageJson as SetRequired<PackageJson, 'version' | 'dependencies'>;
export const getPackageJson = (pathOverride?: string) => {
const packageJsonPath = pathOverride ?? getPackageJsonPath();
const packageJsonRaw = readFileSync(packageJsonPath, 'utf-8');
return JSON.parse(packageJsonRaw) as SetRequired<PackageJson, 'version' | 'dependencies'>;
};

/**
Expand Down Expand Up @@ -86,3 +95,4 @@ export const MOTHERSHIP_GRAPHQL_LINK = process.env.MOTHERSHIP_GRAPHQL_LINK
: 'https://mothership.unraid.net/ws';

export const PM2_HOME = process.env.PM2_HOME ?? join(homedir(), '.pm2');
export const PATHS_CONFIG_MODULES = process.env.PATHS_CONFIG_MODULES!;
5 changes: 4 additions & 1 deletion api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import '@app/dotenv.js';

import { type NestFastifyApplication } from '@nestjs/platform-fastify';
import { unlinkSync } from 'fs';
import { mkdir } from 'fs/promises';
import http from 'http';
import https from 'https';

Expand All @@ -14,7 +15,7 @@ import { WebSocket } from 'ws';

import { logger } from '@app/core/log.js';
import { fileExistsSync } from '@app/core/utils/files/file-exists.js';
import { environment, PORT } from '@app/environment.js';
import { environment, PATHS_CONFIG_MODULES, PORT } from '@app/environment.js';
import * as envVars from '@app/environment.js';
import { setupNewMothershipSubscription } from '@app/mothership/subscribe-to-mothership.js';
import { loadDynamixConfigFile } from '@app/store/actions/load-dynamix-config-file.js';
Expand Down Expand Up @@ -44,6 +45,8 @@ export const viteNodeApp = async () => {
logger.info('ENV %o', envVars);
logger.info('PATHS %o', store.getState().paths);

await mkdir(PATHS_CONFIG_MODULES, { recursive: true });

const cacheable = new CacheableLookup();

Object.assign(global, { WebSocket });
Expand Down
4 changes: 1 addition & 3 deletions api/src/unraid-api/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CacheModule } from '@nestjs/cache-manager';
import { Module } from '@nestjs/common';
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { APP_GUARD } from '@nestjs/core';
import { ThrottlerModule } from '@nestjs/throttler';

import { AuthZGuard } from 'nest-authz';
Expand All @@ -12,7 +12,6 @@ import { AuthModule } from '@app/unraid-api/auth/auth.module.js';
import { AuthenticationGuard } from '@app/unraid-api/auth/authentication.guard.js';
import { CronModule } from '@app/unraid-api/cron/cron.module.js';
import { GraphModule } from '@app/unraid-api/graph/graph.module.js';
import { PluginModule } from '@app/unraid-api/plugin/plugin.module.js';
import { RestModule } from '@app/unraid-api/rest/rest.module.js';
import { UnraidFileModifierModule } from '@app/unraid-api/unraid-file-modifier/unraid-file-modifier.module.js';

Expand Down Expand Up @@ -49,7 +48,6 @@ import { UnraidFileModifierModule } from '@app/unraid-api/unraid-file-modifier/u
},
]),
UnraidFileModifierModule,
PluginModule.register(),
],
controllers: [],
providers: [
Expand Down
6 changes: 5 additions & 1 deletion api/src/unraid-api/cli/cli.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DeveloperCommand } from '@app/unraid-api/cli/developer/developer.comman
import { DeveloperQuestions } from '@app/unraid-api/cli/developer/developer.questions.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
import { LogsCommand } from '@app/unraid-api/cli/logs.command.js';
import { PluginCommandModule } from '@app/unraid-api/cli/plugins/plugin.cli.module.js';
import { PM2Service } from '@app/unraid-api/cli/pm2.service.js';
import { ReportCommand } from '@app/unraid-api/cli/report.command.js';
import { RestartCommand } from '@app/unraid-api/cli/restart.command.js';
Expand All @@ -26,6 +27,9 @@ import { SwitchEnvCommand } from '@app/unraid-api/cli/switch-env.command.js';
import { VersionCommand } from '@app/unraid-api/cli/version.command.js';
import { PluginCliModule } from '@app/unraid-api/plugin/plugin.module.js';

// cli - plugin add/remove
// plugin generator

const DEFAULT_COMMANDS = [
ApiKeyCommand,
ConfigCommand,
Expand Down Expand Up @@ -57,7 +61,7 @@ const DEFAULT_PROVIDERS = [
] as const;

@Module({
imports: [PluginCliModule.register()],
imports: [PluginCliModule.register(), PluginCommandModule],
providers: [...DEFAULT_COMMANDS, ...DEFAULT_PROVIDERS],
})
export class CliModule {}
24 changes: 12 additions & 12 deletions api/src/unraid-api/cli/log.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,39 @@ export class LogService {
return shouldLog;
}

log(message: string): void {
log(...messages: unknown[]): void {
if (this.shouldLog('info')) {
this.logger.log(message);
this.logger.log(...messages);
}
}

info(message: string): void {
info(...messages: unknown[]): void {
if (this.shouldLog('info')) {
this.logger.info(message);
this.logger.info(...messages);
}
}

warn(message: string): void {
warn(...messages: unknown[]): void {
if (this.shouldLog('warn')) {
this.logger.warn(message);
this.logger.warn(...messages);
}
}

error(message: string, trace: string = ''): void {
error(...messages: unknown[]): void {
if (this.shouldLog('error')) {
this.logger.error(message, trace);
this.logger.error(...messages);
}
}

debug(message: any, ...optionalParams: any[]): void {
debug(...messages: unknown[]): void {
if (this.shouldLog('debug')) {
this.logger.debug(message, ...optionalParams);
this.logger.debug(...messages);
}
}

trace(message: any, ...optionalParams: any[]): void {
trace(...messages: unknown[]): void {
if (this.shouldLog('trace')) {
this.logger.log(message, ...optionalParams);
this.logger.log(...messages);
}
}
}
Loading
Loading