Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/tidy-snails-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/apps": patch
---

Fixes app startup failing with EACCESS errors in docker deployments running with specified UIDs
Comment thread
d-gubert marked this conversation as resolved.
Outdated
11 changes: 7 additions & 4 deletions packages/apps/src/server/runtime/deno/AppsEngineDenoRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function getDenoConfigPath(): string {
*
* Returns the path to the generated config file.
*/
function generateEphemeralDenoConfig(targetPath: string, denoConfigPath: string, appsEnginePath: string): void {
function generateEphemeralDenoConfig(targetPath: string, denoConfigPath: string, appsEnginePath: string, packagePath: string): void {
let staticConfig: DenoConfigurationFileSchema;

try {
Expand All @@ -39,6 +39,8 @@ function generateEphemeralDenoConfig(targetPath: string, denoConfigPath: string,
imports: {
...staticConfig.imports,
Comment thread
d-gubert marked this conversation as resolved.
'@rocket.chat/apps-engine/': `${appsEnginePath}/`,
'@rocket.chat/apps/base-runtime/': `${path.join(packagePath, 'base-runtime', 'src')}/`,
Comment thread
d-gubert marked this conversation as resolved.
'@rocket.chat/apps/': `${packagePath}/`,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
};

Expand Down Expand Up @@ -67,7 +69,8 @@ export class DenoRuntimeSubprocessController extends BaseRuntimeSubprocessContro
this.denoDir = process.env.DENO_DIR ?? path.join(this.packagePath, '.deno-cache');

this.denoRuntimePath = path.join(this.tempFilePath, 'deno-runtime', 'main.ts');
Comment thread
d-gubert marked this conversation as resolved.
this.denoEphemeralConfigPath = this.denoConfigPath.replace('.jsonc', '.runtime.jsonc');
// this.denoEphemeralConfigPath = this.denoConfigPath.replace('.jsonc', '.runtime.jsonc');
this.denoEphemeralConfigPath = path.join(this.tempFilePath, 'deno.runtime.jsonc');

Comment thread
coderabbitai[bot] marked this conversation as resolved.
/**
* Deno 2.x refuses to run scripts inside the node_modules, so we create a symlink to the deno runtime files in the temp directory
Expand All @@ -81,8 +84,8 @@ export class DenoRuntimeSubprocessController extends BaseRuntimeSubprocessContro
}
}

// Generate a runtime config with the resolved absolute path for @rocket.chat/apps-engine/
generateEphemeralDenoConfig(this.denoEphemeralConfigPath, this.denoConfigPath, this.appsEnginePath);
// Generate a runtime config with the resolved absolute path for @rocket.chat/apps-engine/ and @rocket.chat/apps/ paths
generateEphemeralDenoConfig(this.denoEphemeralConfigPath, this.denoConfigPath, this.appsEnginePath, this.packagePath);
}

protected buildProcessConfiguration(): ProcessConfiguration {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ describe('DenoRuntimeSubprocessController', () => {
const appPackageBuffer = await fs.readFile(path.join(__dirname, '../../test-data/apps/hello-world-test_0.0.1.zip'));
appPackage = await manager.getParser().unpackageApp(appPackageBuffer);

await fs.unlink(path.join(manager.getTempFilePath(), 'deno-runtime')).catch(function noop() {});

appStorageItem = {
id: 'hello-world-test',
status: AppStatus.MANUALLY_ENABLED,
Expand All @@ -58,13 +56,42 @@ describe('DenoRuntimeSubprocessController', () => {
after(
async () => {
await controller?.stopApp();
await fs.unlink(path.join(manager.getTempFilePath(), 'deno-runtime')).catch((reason) => {
console.warn('Failed to delete temporary Deno runtime symlink', reason);
});
},
{ timeout: 30_000 },
);

it('generates the Deno config in the writable host temp directory', async () => {
// eslint-disable-next-line prefer-destructuring -- accessing a private field for testing
const runtimeTempPath = controller['runtimeTempPath'];
const configPath = path.join(runtimeTempPath, 'deno.runtime.jsonc');
const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
const { args } = controller['buildProcessConfiguration']();
const packagePath = controller['packagePath'].split(path.sep).join(path.posix.sep);

assert.strictEqual(controller['denoEphemeralConfigPath'], configPath);
assert.strictEqual(path.dirname(runtimeTempPath), manager.getTempFilePath());
assert.strictEqual(config.imports['@rocket.chat/apps-engine/'], `${controller['appsEnginePath']}/`);
assert.strictEqual(config.imports['@rocket.chat/apps/base-runtime/'], `${packagePath}/base-runtime/src/`);
assert.strictEqual(config.imports['@rocket.chat/apps/'], `${packagePath}/`);
assert.strictEqual(config.imports['@std/io'], 'jsr:@std/io@^0.225.3');
assert.ok(args.includes(`--config=${configPath}`));
});

it('uses an isolated runtime directory for each controller and removes it on stop', async () => {
const secondController = new DenoRuntimeSubprocessController(manager, appPackage, appStorageItem);
Comment thread
AlgoArtist06 marked this conversation as resolved.
Outdated
// eslint-disable-next-line prefer-destructuring -- accessing a private field for testing
const runtimeTempPath = secondController['runtimeTempPath'];

try {
assert.notStrictEqual(runtimeTempPath, controller['runtimeTempPath']);
assert.strictEqual((await fs.stat(runtimeTempPath)).isDirectory(), true);
} finally {
await secondController.stopApp();
}

await assert.rejects(fs.stat(runtimeTempPath), { code: 'ENOENT' });
});

it('correctly identifies a call to the HTTP accessor', { timeout: 15_000 }, async () => {
const httpBridge = manager.getBridges().getHttpBridge();
const doCallSpy = mock.method(httpBridge, 'doCall');
Expand Down
Loading