Skip to content

Commit a8566e9

Browse files
authored
build: fix doinst.sh compatibility with installpkg --root (#1446)
- Isolate plugin concerns into `.plg` plugin & api file modifiers instead of the api's slackware package. ## Summary by CodeRabbit * **New Features** * Installation process modularized into package installation, post-install setup with verification, and service startup with cleanup. * Added logging and error detection during installation, including symlink verification. * Created required log directory to support service dependencies. * Integrated nginx service with reload capability triggered by configuration changes. * Added automatic patching of nginx configuration and hosts file to improve security and iframe compatibility. * Enhanced file modification system to handle side effects and trigger nginx reloads as needed. * **Refactor** * Restructured installation scripts for clarity; setup scripts now separate steps. * Removed legacy installation logic and deprecated related scripts. * Enabled hard link addition during package creation. * Simplified installation scripts by removing conditional branching and detailed logging. * Streamlined API environment setup by removing redundant post-install steps. * Updated dependency injection to explicitly provide nginx service token. * Improved patch application error reporting with file path details. * **Chores** * Disabled legacy scripts to streamline installation. * Removed `.gitignore` to track previously ignored files. * Updated tests to include new dependencies and relaxed logger assertions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1210661184127051
1 parent f73e5e0 commit a8566e9

File tree

25 files changed

+284
-266
lines changed

25 files changed

+284
-266
lines changed

api/src/environment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export const MOTHERSHIP_GRAPHQL_LINK = process.env.MOTHERSHIP_GRAPHQL_LINK
9999
export const PM2_HOME = process.env.PM2_HOME ?? join(homedir(), '.pm2');
100100
export const PM2_PATH = join(import.meta.dirname, '../../', 'node_modules', 'pm2', 'bin', 'pm2');
101101
export const ECOSYSTEM_PATH = join(import.meta.dirname, '../../', 'ecosystem.config.json');
102+
export const LOGS_DIR = process.env.LOGS_DIR ?? '/var/log/unraid-api';
102103

103104
export const PATHS_CONFIG_MODULES =
104105
process.env.PATHS_CONFIG_MODULES ?? '/boot/config/plugins/dynamix.my.servers/configs';

api/src/unraid-api/cli/pm2.service.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { Injectable } from '@nestjs/common';
22
import { existsSync } from 'node:fs';
3-
import { rm } from 'node:fs/promises';
3+
import { mkdir, rm } from 'node:fs/promises';
44
import { join } from 'node:path';
55

66
import type { Options, Result, ResultPromise } from 'execa';
77
import { execa, ExecaError } from 'execa';
88

9-
import { PM2_HOME, PM2_PATH } from '@app/environment.js';
9+
import { LOGS_DIR, PM2_HOME, PM2_PATH } from '@app/environment.js';
1010
import { LogService } from '@app/unraid-api/cli/log.service.js';
1111

1212
type CmdContext = Options & {
@@ -97,4 +97,11 @@ export class PM2Service {
9797
this.logger.trace('PM2 home directory does not exist.');
9898
}
9999
}
100+
101+
/**
102+
* Ensures that the dependencies necessary for PM2 to start and operate are present.
103+
*/
104+
async ensurePm2Dependencies() {
105+
await mkdir(LOGS_DIR, { recursive: true });
106+
}
100107
}

api/src/unraid-api/cli/start.command.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export class StartCommand extends CommandRunner {
2020
}
2121

2222
async cleanupPM2State() {
23+
await this.pm2.ensurePm2Dependencies();
2324
await this.pm2.run({ tag: 'PM2 Stop' }, 'stop', ECOSYSTEM_PATH);
2425
await this.pm2.run({ tag: 'PM2 Update' }, 'update');
2526
await this.pm2.deleteDump();
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Module } from '@nestjs/common';
2+
3+
import { NginxService } from '@app/unraid-api/nginx/nginx.service.js';
4+
5+
/**
6+
*
7+
*/
8+
@Module({
9+
providers: [NginxService],
10+
exports: [NginxService],
11+
})
12+
export class NginxModule {}

api/src/unraid-api/plugin/global-deps.module.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,22 @@ import { GRAPHQL_PUBSUB_TOKEN } from '@unraid/shared/pubsub/graphql.pubsub.js';
55
import {
66
API_KEY_SERVICE_TOKEN,
77
LIFECYCLE_SERVICE_TOKEN,
8+
NGINX_SERVICE_TOKEN,
89
UPNP_CLIENT_TOKEN,
910
} from '@unraid/shared/tokens.js';
1011

1112
import { pubsub } from '@app/core/pubsub.js';
1213
import { LifecycleService } from '@app/unraid-api/app/lifecycle.service.js';
1314
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
1415
import { ApiKeyModule } from '@app/unraid-api/graph/resolvers/api-key/api-key.module.js';
16+
import { NginxModule } from '@app/unraid-api/nginx/nginx.module.js';
17+
import { NginxService } from '@app/unraid-api/nginx/nginx.service.js';
1518
import { upnpClient } from '@app/upnp/helpers.js';
1619

1720
// This is the actual module that provides the global dependencies
1821
@Global()
1922
@Module({
20-
imports: [ApiKeyModule],
23+
imports: [ApiKeyModule, NginxModule],
2124
providers: [
2225
{
2326
provide: UPNP_CLIENT_TOKEN,
@@ -31,6 +34,10 @@ import { upnpClient } from '@app/upnp/helpers.js';
3134
provide: API_KEY_SERVICE_TOKEN,
3235
useClass: ApiKeyService,
3336
},
37+
{
38+
provide: NGINX_SERVICE_TOKEN,
39+
useClass: NginxService,
40+
},
3441
PrefixedID,
3542
LifecycleService,
3643
{
@@ -42,6 +49,7 @@ import { upnpClient } from '@app/upnp/helpers.js';
4249
UPNP_CLIENT_TOKEN,
4350
GRAPHQL_PUBSUB_TOKEN,
4451
API_KEY_SERVICE_TOKEN,
52+
NGINX_SERVICE_TOKEN,
4553
PrefixedID,
4654
LIFECYCLE_SERVICE_TOKEN,
4755
LifecycleService,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Injectable } from '@nestjs/common';
2+
3+
import { NginxService } from '@app/unraid-api/nginx/nginx.service.js';
4+
import { ModificationEffect } from '@app/unraid-api/unraid-file-modifier/file-modification.js';
5+
6+
@Injectable()
7+
export class FileModificationEffectService {
8+
constructor(private readonly nginxService: NginxService) {}
9+
async runEffect(effect: ModificationEffect): Promise<void> {
10+
switch (effect) {
11+
case 'nginx:reload':
12+
await this.nginxService.reload();
13+
break;
14+
}
15+
}
16+
}

api/src/unraid-api/unraid-file-modifier/file-modification.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,17 @@ import { coerce, compare, gte } from 'semver';
88

99
import { getUnraidVersion } from '@app/common/dashboard/get-unraid-version.js';
1010

11+
export type ModificationEffect = 'nginx:reload';
12+
1113
export interface ShouldApplyWithReason {
1214
shouldApply: boolean;
1315
reason: string;
16+
/**
17+
* Effectful actions that should be performed after the modification is applied.
18+
*
19+
* This field helps ensure that an effect requested by multiple modifications is only performed once.
20+
*/
21+
effects?: ModificationEffect[];
1422
}
1523

1624
// Convert interface to abstract class with default implementations
@@ -98,7 +106,7 @@ export abstract class FileModification {
98106
const currentContent = await readFile(this.filePath, 'utf8').catch(() => '');
99107
const parsedPatch = parsePatch(patchContents)[0];
100108
if (!parsedPatch?.hunks.length) {
101-
throw new Error('Invalid Patch Format: No hunks found');
109+
throw new Error(`Invalid Patch Format: No hunks found for ${this.filePath}`);
102110
}
103111

104112
const results = applyPatch(currentContent, parsedPatch);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { constants } from 'fs';
2+
import { access } from 'fs/promises';
3+
import { readFile } from 'node:fs/promises';
4+
5+
import {
6+
FileModification,
7+
ShouldApplyWithReason,
8+
} from '@app/unraid-api/unraid-file-modifier/file-modification.js';
9+
10+
export default class HostsModification extends FileModification {
11+
id: string = 'hosts';
12+
public readonly filePath: string = '/etc/hosts' as const;
13+
14+
protected async generatePatch(overridePath?: string): Promise<string> {
15+
const originalContent = await readFile(this.filePath, 'utf8');
16+
17+
// Use a case-insensitive word-boundary regex so the hostname must appear as an independent token
18+
// prevents partial string & look-alike conflicts such as "keys.lime-technology.com.example.com"
19+
const hostPattern = /\bkeys\.lime-technology\.com\b/i;
20+
21+
const newContent = originalContent
22+
.split('\n')
23+
.filter((line) => !hostPattern.test(line))
24+
.join('\n');
25+
26+
return this.createPatchWithDiff(overridePath ?? this.filePath, originalContent, newContent);
27+
}
28+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { readFile } from 'node:fs/promises';
2+
3+
import {
4+
FileModification,
5+
ShouldApplyWithReason,
6+
} from '@app/unraid-api/unraid-file-modifier/file-modification.js';
7+
8+
export default class NginxConfModification extends FileModification {
9+
id: string = 'nginx-conf';
10+
public readonly filePath: string = '/etc/nginx/nginx.conf' as const;
11+
12+
protected async generatePatch(overridePath?: string): Promise<string> {
13+
const originalContent = await readFile(this.filePath, 'utf8');
14+
const newContent = originalContent.replace(
15+
"add_header X-Frame-Options 'SAMEORIGIN';",
16+
'add_header Content-Security-Policy "frame-ancestors \'self\' https://connect.myunraid.net/";'
17+
);
18+
return this.createPatchWithDiff(overridePath ?? this.filePath, originalContent, newContent);
19+
}
20+
21+
async shouldApply(): Promise<ShouldApplyWithReason> {
22+
const superShouldApply = await super.shouldApply();
23+
if (!superShouldApply.shouldApply) {
24+
return superShouldApply;
25+
}
26+
const content = await readFile(this.filePath, 'utf8');
27+
const hasSameOrigin = content.includes("add_header X-Frame-Options 'SAMEORIGIN';");
28+
if (!hasSameOrigin) {
29+
return {
30+
shouldApply: false,
31+
reason: 'X-Frame-Options SAMEORIGIN header not found in nginx.conf',
32+
};
33+
}
34+
return {
35+
shouldApply: true,
36+
reason: 'X-Frame-Options SAMEORIGIN found and needs to be replaced with Content-Security-Policy',
37+
effects: ['nginx:reload'],
38+
};
39+
}
40+
}

0 commit comments

Comments
 (0)