blob: ca004b78d6fb432d95e48c34be35de588bebb746 [file]
/*
* 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 { spawn } from 'node:child_process';
import { cp, mkdtemp, realpath, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { compareProductReleaseVersions } from '@maka/runtime-host/operator/update-package-evidence';
import { resolveRuntimeHostNpmGlobalInstallation } from './runtime-host-cli-installation.js';
import type { RuntimeHostUpdateSelector } from './runtime-host-cli.js';
import { resolveRuntimeHostRegistryUpdateCandidate } from './runtime-host-registry-update.js';
import { withRuntimeHostRegistryUpdateArchive } from './runtime-host-update-package.js';
import type { RuntimeHostInstalledUpdateExpectedSource } from './runtime-host-installed-update-coordinator.js';
interface RuntimeHostInstalledUpdateBootstrapDeps {
readonly resolveInstallation: typeof resolveRuntimeHostNpmGlobalInstallation;
readonly resolveCandidate: typeof resolveRuntimeHostRegistryUpdateCandidate;
readonly withArchive: typeof withRuntimeHostRegistryUpdateArchive;
readonly runCoordinator: (input: RuntimeHostInstalledUpdateCoordinatorLaunch) => Promise<number>;
}
interface RuntimeHostInstalledUpdateCoordinatorLaunch {
readonly coordinatorCliPath: string;
readonly rootPath: string;
readonly archivePath: string;
readonly installedPackageRoot: string;
readonly installedCliPath: string;
readonly currentVersion: string;
readonly targetVersion: string;
readonly targetIntegrity: string;
readonly targetCompatibility?: number;
readonly allowInterruptActiveTasks: boolean;
readonly expectedSource?: RuntimeHostInstalledUpdateExpectedSource;
}
export async function runRuntimeHostInstalledUpdateBootstrap(
input: {
readonly rootPath: string;
readonly selector: RuntimeHostUpdateSelector;
readonly allowInterruptActiveTasks: boolean;
readonly expectedSource?: RuntimeHostInstalledUpdateExpectedSource;
},
overrides: Partial<RuntimeHostInstalledUpdateBootstrapDeps> = {},
): Promise<number> {
const deps: RuntimeHostInstalledUpdateBootstrapDeps = {
resolveInstallation: resolveRuntimeHostNpmGlobalInstallation,
resolveCandidate: resolveRuntimeHostRegistryUpdateCandidate,
withArchive: withRuntimeHostRegistryUpdateArchive,
runCoordinator: launchCoordinator,
...overrides,
};
const installation = await deps.resolveInstallation();
const target = await deps.resolveCandidate(input.selector);
if (compareProductReleaseVersions(target.version, installation.observedRelease.version) < 0) {
throw new Error(
`Downgrading Maka from ${installation.observedRelease.version} to ${target.version} is not supported`,
);
}
return deps.withArchive(target, async (archivePath) => {
const temporaryRoot = await mkdtemp(join(tmpdir(), 'maka-installed-update-coordinator-'));
try {
const coordinatorPackageRoot = join(temporaryRoot, 'maka-agent');
await cp(installation.observedRelease.packageRoot, coordinatorPackageRoot, {
recursive: true,
force: false,
errorOnExist: true,
preserveTimestamps: true,
});
const coordinatorCliPath = await realpath(join(coordinatorPackageRoot, 'dist', 'cli.js'));
if (!(await stat(coordinatorCliPath)).isFile()) {
throw new Error('The copied Maka update coordinator has no CLI entry point');
}
return deps.runCoordinator({
coordinatorCliPath,
rootPath: input.rootPath,
archivePath,
installedPackageRoot: installation.observedRelease.packageRoot,
installedCliPath: installation.observedRelease.cliPath,
currentVersion: installation.observedRelease.version,
targetVersion: target.version,
targetIntegrity: target.integrity,
...(target.compatibility === undefined
? {}
: { targetCompatibility: target.compatibility }),
allowInterruptActiveTasks: input.allowInterruptActiveTasks,
...(input.expectedSource ? { expectedSource: input.expectedSource } : {}),
});
} finally {
await rm(temporaryRoot, { recursive: true, force: true }).catch(() => undefined);
}
});
}
function launchCoordinator(input: RuntimeHostInstalledUpdateCoordinatorLaunch): Promise<number> {
const args = [
input.coordinatorCliPath,
'runtime-host',
'local-update-apply',
'--root',
input.rootPath,
'--archive',
input.archivePath,
'--installed-package-root',
input.installedPackageRoot,
'--installed-cli-path',
input.installedCliPath,
'--current-version',
input.currentVersion,
'--target-version',
input.targetVersion,
'--target-integrity',
input.targetIntegrity,
...(input.targetCompatibility === undefined
? []
: ['--target-compatibility', String(input.targetCompatibility)]),
...(input.allowInterruptActiveTasks ? ['--allow-interrupt-active-tasks'] : []),
...(input.expectedSource
? [
'--expected-root-id',
input.expectedSource.rootId,
'--expected-deployment-revision',
input.expectedSource.deploymentRevision,
'--expected-owner-installation-id',
input.expectedSource.ownerInstallationId,
'--expected-host-epoch',
input.expectedSource.hostEpoch,
]
: []),
];
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, args, { stdio: 'inherit', windowsHide: false });
child.once('error', reject);
child.once('close', (code, signal) => {
if (signal) reject(new Error(`Maka update coordinator exited on ${signal}`));
else resolve(code ?? 1);
});
});
}