Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
156 changes: 156 additions & 0 deletions packages/cli/src/__tests__/runtime-host-openrc-service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import assert from 'node:assert/strict';
import { execFile } from 'node:child_process';
import { lstat, mkdir, mkdtemp, readFile, rm, symlink, unlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';
import { test } from 'node:test';
import { createOpenRcRuntimeHostLifecycleProvider } from '../runtime-host-openrc-service.js';

const SERVICE_ID = 'a'.repeat(64);
const SERVICE_NAME = `maka-runtime-host-${SERVICE_ID}`;
const UPDATE_NAME = `${SERVICE_NAME}-update`;

test('OpenRC provider owns one supervised Host and reconciliation loop', async (t) => {
const root = await mkdtemp(join(tmpdir(), 'maka openrc provider-'));
t.after(() => rm(root, { recursive: true, force: true }));
const paths = {
initDirectory: join(root, 'init.d'),
runlevelDirectory: join(root, 'runlevels', 'default'),
artifactDirectory: join(root, 'artifacts'),
logDirectory: join(root, 'logs'),
stateDirectory: join(root, 'state'),
};
const active = new Set<string>();
const calls: [string, readonly string[]][] = [];
let runlevel = 'sysinit';
const runCommand = async (command: string, args: readonly string[]) => {
calls.push([command, args]);
if (command === 'supervise-daemon' && args[0] === '--help') {
return { exitCode: 1, stdout: '', stderr: 'usage' };
}
if (command === 'rc-service' && args[0] !== '--help') {
const [name, action] = args;
if (action === 'status') {
return { exitCode: active.has(name!) ? 0 : 3, stdout: '', stderr: '' };
}
if (action === 'start') {
active.add(name!);
const state = join(paths.stateDirectory, 'options', name!);
await mkdir(state, { recursive: true });
await writeFile(join(state, 'child_pid'), name === SERVICE_NAME ? '4242\n' : '4343\n');
} else if (action === 'stop') {
active.delete(name!);
}
return { exitCode: 0, stdout: '', stderr: '' };
}
if (command === 'rc-update' && args[0] !== 'show') {
const [action, name] = args;
const link = join(paths.runlevelDirectory, name!);
if (action === 'add') {
await mkdir(paths.runlevelDirectory, { recursive: true });
await symlink(join(paths.initDirectory, name!), link);
} else if (action === 'del') {
await unlink(link);
}
}
return {
exitCode: 0,
stdout: command === 'rc-status' ? `${runlevel}\n` : '',
stderr: '',
};
};
const provider = createOpenRcRuntimeHostLifecycleProvider(SERVICE_ID, 'openrc_system', {
uid: 0,
paths,
runCommand,
});
const supervisor = {
command: [
process.execPath,
'/tmp/maka cli.js',
"quote'value",
'$(printf injected)',
'*.js',
] as const,
};
const reconciliation = {
command: ['/tmp/maka operator', 'reconcile-update', '--framed'] as const,
};

await assert.rejects(provider.supervisor.preflight(), { code: 'service_manager_unavailable' });
runlevel = 'default';
await provider.supervisor.preflight();
await provider.supervisor.converge(supervisor);
await provider.reconciliationTrigger.converge(reconciliation);
await provider.supervisor.verify(supervisor);
await provider.reconciliationTrigger.verify(reconciliation);
const servicePath = join(paths.initDirectory, SERVICE_NAME);
const evaluated = await promisify(execFile)('/bin/sh', [
'-c',
'. "$1"; eval "set -- --stdout $output_log --stderr $error_log $command -- $command_args"; printf "%s\\n" "$@"',
'sh',
servicePath,
]);
assert.deepEqual(evaluated.stdout.trimEnd().split('\n'), [
'--stdout',
join(paths.logDirectory, 'host.stdout.log'),
'--stderr',
join(paths.logDirectory, 'host.stderr.log'),
process.execPath,
'--',
'/tmp/maka cli.js',
"quote'value",
'$(printf injected)',
'*.js',
]);
await provider.supervisor.activate();
await provider.reconciliationTrigger.activate();

assert.deepEqual(await provider.supervisor.status(), {
provider: 'openrc_system',
installed: true,
enabled: true,
active: true,
state: 'running',
pid: 4242,
lastExitCode: null,
});
assert.deepEqual(await provider.reconciliationTrigger.status(), {
installed: true,
active: true,
});
assert.match(await readFile(servicePath, 'utf8'), /retry=TERM\/20\/KILL\/5[\s\S]*respawn_max=0/u);
assert.match(
await readFile(join(paths.artifactDirectory, 'update'), 'utf8'),
/reconcile-update[\s\S]*sleep 86400/u,
);
await writeFile(join(paths.logDirectory, 'host.stdout.log'), 'host output\n');
assert.match(await provider.supervisor.logs(), /host output/u);

await provider.reconciliationTrigger.uninstall();
await provider.supervisor.uninstall();
assert.equal(active.size, 0);
await assert.rejects(lstat(join(paths.initDirectory, SERVICE_NAME)), { code: 'ENOENT' });
await assert.rejects(lstat(join(paths.initDirectory, UPDATE_NAME)), { code: 'ENOENT' });
assert.ok(calls.some(([command, args]) => command === 'rc-update' && args[0] === 'add'));
});
100 changes: 81 additions & 19 deletions packages/cli/src/__tests__/runtime-host-setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,9 @@ import { runRuntimeHostSetupCli } from '../runtime-host-setup-command.js';
import { RuntimeHostAccessUnavailableError } from '../runtime-host-access-command.js';
import { replaceRuntimeHostLifecycle } from '../runtime-host-lifecycle-transaction.js';
import { manageRuntimeHostManagedLifecycle } from '../runtime-host-managed-lifecycle-manager.js';
import {
resolveRuntimeHostLifecycleProvider,
selectRuntimeHostLifecycleProvider,
} from '../runtime-host-service-management-command.js';
import { resolveRuntimeHostLifecycleProvider } from '../runtime-host-service-management-command.js';
import {
resolveRuntimeHostManagedServiceId,
RuntimeHostServiceManagerError,
type RuntimeHostServiceBackend,
} from '../runtime-host-service-manager.js';

Expand Down Expand Up @@ -337,6 +333,82 @@ test('on-demand setup installs one exact deployment without a service backend',
assert.equal(uninstalled.retirement.kind, 'stopped');
});

test('fresh supervised setup discovers its provider before constructing a legacy backend', async (t) => {
const base = await realpath(await mkdtemp(join(tmpdir(), 'maka-runtime-host-supervised-setup-')));
const stateRoot = join(base, 'state');
const clientDataRoot = join(base, 'client');
let rootId = '';
t.after(async () => {
await Promise.all([
rm(base, { recursive: true, force: true }),
rootId
? rm(join(resolveRootControlNamespace(), rootId), { recursive: true, force: true })
: Promise.resolve(),
rootId
? rm(join(resolveRootOwnershipNamespace(), `${rootId}.lock`), { force: true })
: Promise.resolve(),
]);
});

const result = await runRuntimeHostSetupCli(
{
json: true,
lifecycle: 'supervised',
clientDataRoot,
defaultRootPath: stateRoot,
sourcePackageRoot: base,
version: '1.2.3',
principalId: 'desktop:client-1',
preset: 'desktop-client',
},
{
createBackend: () => assert.fail('a fresh canonical setup has no legacy backend'),
discoverLifecycleProvider: async (discoveredRootId) => {
rootId = discoveredRootId;
return {
provider: resolveRuntimeHostLifecycleProvider(rootId, 'openrc_user'),
availability: 'session',
};
},
resolveRegistryCandidate: async () => ({
kind: 'npm_registry',
version: '1.2.3',
integrity: PACKAGE_INTEGRITY,
}),
withRegistryPackage: async (_candidate, use) => use('/verified/package'),
prepareDeployment: async ({ serviceId }) => ({
version: '1.2.3',
root: join(base, 'deployment'),
cliPath: '/verified/package/dist/cli.js',
operatorPath: '/opt/maka/operator',
activate: async () => undefined,
cleanup: async () => undefined,
rollback: async () => undefined,
}),
allocateLoopbackPort: async () => 43_210,
replaceLifecycle: async ({ desired }) => {
assert.equal(desired.lifecycle.mode, 'supervised');
assert.equal(desired.lifecycle.provider, 'openrc_user');
return { kind: 'replaced', config: desired };
},
prunePackages: async () => undefined,
replaceCredential: async () => ({
rootId,
credential: 'secret-token',
credentialId: 'credential-1',
principalKind: 'remote_owner',
principalId: 'desktop:client-1',
operationGrants: [],
canPublishClientCapabilities: false,
canUseHostPaths: false,
}),
verifyCredential: async () => undefined,
writeOutput: () => undefined,
},
);
assert.equal(result, 0);
});

test('managed setup frames reject malformed machine output', () => {
assert.equal(
decodeRuntimeHostSetupFrame(
Expand Down Expand Up @@ -368,20 +440,10 @@ test('managed setup frames reject malformed machine output', () => {
);
});

test('lifecycle discovery records environment scope and persisted providers are never reselected', () => {
assert.deepEqual(
selectRuntimeHostLifecycleProvider({
platform: 'linux',
environment: { WSL_DISTRO_NAME: 'Ubuntu' },
}),
{ provider: 'systemd_user', availability: 'environment' },
);
assert.throws(
() => resolveRuntimeHostLifecycleProvider('a'.repeat(64), 'openrc_user'),
(error: unknown) =>
error instanceof RuntimeHostServiceManagerError &&
error.code === 'service_manager_unavailable',
);
test('persisted OpenRC providers resolve without reselecting the platform default', () => {
const openRc = resolveRuntimeHostLifecycleProvider('a'.repeat(64), 'openrc_user');
assert.equal(openRc.supervisor.provider, 'openrc_user');
assert.equal(openRc.reconciliationTrigger.provider, 'openrc_supervised_loop');
});

test('registry package identity avoids local content and recovers an interrupted removal', async (t) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/runtime-host-managed-lifecycle-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ function presentationManager(
config: RuntimeHostManagedDeploymentConfig,
): Exclude<RuntimeHostManagedServiceStatus['manager'], 'none'> {
if (config.lifecycle.mode === 'on_demand') return 'on_demand';
return config.lifecycle.provider === 'systemd_user' ? 'systemd_user' : 'launch_agent';
return config.lifecycle.provider;
}

function projectLegacyConfig(
Expand Down
Loading
Loading