mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
489 lines
14 KiB
TypeScript
489 lines
14 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import { readPrivateLocalCommandFile } from '@qinglong/local-command-file';
|
|
|
|
import { currentIdentity } from '../foundation/contract';
|
|
import { LocalDeploymentConfigurationError } from '../foundation/error';
|
|
import {
|
|
ensurePrivateDirectory,
|
|
preflightPublishedFile,
|
|
publishExactFile,
|
|
validatePrivateDirectory,
|
|
} from '../foundation/files';
|
|
import {
|
|
normalizeLocalDeploymentAdoptedBundleCommand,
|
|
type LocalDeploymentAdoptedBundleCommand,
|
|
type LocalDeploymentAdoptedBundleOperation,
|
|
} from './contract';
|
|
import {
|
|
renderLocalDeploymentAdoptedBundleMaterial,
|
|
verifyLocalDeploymentAdoptedEvidence,
|
|
} from './material';
|
|
|
|
export interface LocalDeploymentAdoptedBundleResult {
|
|
readonly schemaVersion: 1;
|
|
readonly operation: LocalDeploymentAdoptedBundleOperation;
|
|
readonly status: 'prepared' | 'existing' | 'verified';
|
|
readonly bundleId: string;
|
|
readonly bundleDigest: string;
|
|
readonly profile: 'edge' | 'standalone';
|
|
readonly service: Readonly<{
|
|
kind: 'systemd' | 'openrc' | 'compose';
|
|
status: 'prepared' | 'existing' | 'verified';
|
|
}>;
|
|
readonly applicationConfiguration: Readonly<{
|
|
schema: 'qinglong/local-application-process@v4';
|
|
status: 'prepared' | 'existing' | 'verified';
|
|
}>;
|
|
readonly directories: Readonly<{
|
|
created: number;
|
|
existing: number;
|
|
}>;
|
|
}
|
|
|
|
function configurationError(message: string, cause?: unknown): never {
|
|
throw new LocalDeploymentConfigurationError(message, { cause });
|
|
}
|
|
|
|
function validatePrivateFile(
|
|
filePath: string,
|
|
uid: number,
|
|
gid: number,
|
|
label: string,
|
|
): void {
|
|
try {
|
|
const stat = fs.lstatSync(filePath);
|
|
if (
|
|
!stat.isFile() ||
|
|
stat.isSymbolicLink() ||
|
|
stat.uid !== uid ||
|
|
stat.gid !== gid ||
|
|
(stat.mode & 0o777) !== 0o600 ||
|
|
stat.nlink !== 1 ||
|
|
stat.size < 2 ||
|
|
fs.realpathSync(filePath) !== filePath
|
|
) {
|
|
configurationError(`${label} identity is invalid`);
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof LocalDeploymentConfigurationError) throw error;
|
|
configurationError(`${label} is unavailable`, error);
|
|
}
|
|
}
|
|
|
|
function stagePath(targetPath: string): string {
|
|
return path.join(
|
|
path.dirname(targetPath),
|
|
`.${path.basename(targetPath)}.ql3-deploy-stage`,
|
|
);
|
|
}
|
|
|
|
function verifyPublishedFile(
|
|
filePath: string,
|
|
expected: string,
|
|
mode: number,
|
|
uid: number,
|
|
gid: number,
|
|
label: string,
|
|
): void {
|
|
try {
|
|
const stat = fs.lstatSync(filePath);
|
|
if (
|
|
!stat.isFile() ||
|
|
stat.isSymbolicLink() ||
|
|
stat.uid !== uid ||
|
|
stat.gid !== gid ||
|
|
(stat.mode & 0o777) !== mode ||
|
|
stat.nlink !== 1 ||
|
|
stat.size !== Buffer.byteLength(expected, 'utf8') ||
|
|
fs.realpathSync(filePath) !== filePath ||
|
|
fs.existsSync(stagePath(filePath)) ||
|
|
fs.readFileSync(filePath, 'utf8') !== expected
|
|
) {
|
|
configurationError(`${label} terminal material drifted`);
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof LocalDeploymentConfigurationError) throw error;
|
|
configurationError(`${label} cannot be verified`, error);
|
|
}
|
|
}
|
|
|
|
function rejectAlternateDescriptors(
|
|
selectedPath: string,
|
|
serviceRoot: string,
|
|
): void {
|
|
for (const fileName of [
|
|
'qinglong3.service',
|
|
'qinglong3.openrc',
|
|
'compose.yaml',
|
|
]) {
|
|
const candidate = path.join(serviceRoot, fileName);
|
|
if (candidate !== selectedPath && fs.existsSync(candidate)) {
|
|
configurationError('an alternate service descriptor already exists');
|
|
}
|
|
}
|
|
}
|
|
|
|
function validateExistingPrerequisites(
|
|
root: string,
|
|
serviceRoot: string,
|
|
ownerPepperKeyring: string,
|
|
ownerPepperBackup: string,
|
|
secretKeyring: string,
|
|
commitmentRoot: string,
|
|
uid: number,
|
|
gid: number,
|
|
): void {
|
|
validatePrivateDirectory(root, uid, 'deploymentRoot');
|
|
validatePrivateDirectory(
|
|
ownerPepperKeyring,
|
|
uid,
|
|
'ownerPepperKeyringDirectory',
|
|
);
|
|
validatePrivateDirectory(
|
|
ownerPepperBackup,
|
|
uid,
|
|
'ownerPepperBackupDirectory',
|
|
);
|
|
validatePrivateDirectory(serviceRoot, uid, 'serviceDescriptorRoot');
|
|
validatePrivateDirectory(
|
|
path.join(serviceRoot, 'cutovers'),
|
|
uid,
|
|
'cutoverRoot',
|
|
);
|
|
validatePrivateDirectory(commitmentRoot, uid, 'cutoverJournalRoot');
|
|
validatePrivateFile(secretKeyring, uid, gid, 'localSecretKeyring');
|
|
}
|
|
|
|
export function prepareLocalDeploymentAdoptedBundle(
|
|
input: unknown,
|
|
): Readonly<LocalDeploymentAdoptedBundleResult> {
|
|
const command = normalizeLocalDeploymentAdoptedBundleCommand(
|
|
input,
|
|
'local.deployment.adopted.prepare',
|
|
);
|
|
const identity = currentIdentity();
|
|
const evidence = verifyLocalDeploymentAdoptedEvidence(
|
|
command,
|
|
identity.uid,
|
|
identity.gid,
|
|
);
|
|
const material = renderLocalDeploymentAdoptedBundleMaterial(
|
|
command,
|
|
evidence,
|
|
identity.uid,
|
|
identity.gid,
|
|
);
|
|
validateExistingPrerequisites(
|
|
command.options.deploymentRoot,
|
|
material.paths.service,
|
|
material.paths.ownerPepperKeyring,
|
|
material.paths.ownerPepperBackup,
|
|
material.paths.secretKeyring,
|
|
path.dirname(command.request.cutover.commitmentPath),
|
|
identity.uid,
|
|
identity.gid,
|
|
);
|
|
rejectAlternateDescriptors(material.paths.descriptor, material.paths.service);
|
|
if (
|
|
command.options.service.kind !== 'compose' &&
|
|
(fs.existsSync(material.paths.composeSelection) ||
|
|
fs.existsSync(material.paths.composeRevisions))
|
|
) {
|
|
configurationError('process service cannot inherit Compose material');
|
|
}
|
|
preflightPublishedFile(
|
|
material.paths.applicationConfig,
|
|
material.applicationConfig,
|
|
0o600,
|
|
identity.uid,
|
|
'adopted application configuration',
|
|
);
|
|
preflightPublishedFile(
|
|
material.paths.descriptor,
|
|
material.descriptor.contents,
|
|
material.descriptor.mode,
|
|
identity.uid,
|
|
'adopted service descriptor',
|
|
);
|
|
preflightPublishedFile(
|
|
material.paths.bundleReceipt,
|
|
material.receiptContents,
|
|
0o600,
|
|
identity.uid,
|
|
'adopted bundle receipt',
|
|
);
|
|
if (material.composeSelection !== null) {
|
|
preflightPublishedFile(
|
|
material.paths.composeRevision,
|
|
material.composeSelection,
|
|
0o600,
|
|
identity.uid,
|
|
'initial adopted compose revision',
|
|
);
|
|
preflightPublishedFile(
|
|
material.paths.composeSelection,
|
|
material.composeSelection,
|
|
0o600,
|
|
identity.uid,
|
|
'active adopted compose selection',
|
|
);
|
|
}
|
|
const directoryPaths = [
|
|
[material.paths.receipts, 'receiptRoot'],
|
|
[material.paths.artifacts, 'artifactRoot'],
|
|
[material.paths.pluginStaging, 'pluginStagingRoot'],
|
|
[material.paths.pluginActivation, 'pluginActivationRoot'],
|
|
...(material.composeSelection === null
|
|
? []
|
|
: ([
|
|
[material.paths.composeRevisions, 'composeRevisionRoot'],
|
|
[material.paths.composeRollouts, 'composeRolloutRoot'],
|
|
[material.paths.composeRolloutBackups, 'composeRolloutBackupRoot'],
|
|
[material.paths.composeRestores, 'composeRestoreRoot'],
|
|
[
|
|
material.paths.composeRestoreSafeguards,
|
|
'composeRestoreSafeguardRoot',
|
|
],
|
|
[
|
|
material.paths.composeEvidenceCollections,
|
|
'composeEvidenceCollectionRoot',
|
|
],
|
|
[
|
|
material.paths.composeCollectedEvidence,
|
|
'composeCollectedEvidenceRoot',
|
|
],
|
|
[
|
|
material.paths.composeCollectedRolloutBackups,
|
|
'composeCollectedRolloutBackupRoot',
|
|
],
|
|
[
|
|
material.paths.composeCollectedRestoreSafeguards,
|
|
'composeCollectedRestoreSafeguardRoot',
|
|
],
|
|
] as const)),
|
|
] as const;
|
|
const directoryStatuses = directoryPaths.map(([directory, label]) =>
|
|
ensurePrivateDirectory(directory, identity.uid, label),
|
|
);
|
|
const applicationStatus = publishExactFile(
|
|
material.paths.applicationConfig,
|
|
material.applicationConfig,
|
|
0o600,
|
|
identity.uid,
|
|
'adopted application configuration',
|
|
);
|
|
const serviceStatus = publishExactFile(
|
|
material.paths.descriptor,
|
|
material.descriptor.contents,
|
|
material.descriptor.mode,
|
|
identity.uid,
|
|
'adopted service descriptor',
|
|
);
|
|
const composeRevisionStatus =
|
|
material.composeSelection === null
|
|
? 'existing'
|
|
: publishExactFile(
|
|
material.paths.composeRevision,
|
|
material.composeSelection,
|
|
0o600,
|
|
identity.uid,
|
|
'initial adopted compose revision',
|
|
);
|
|
const composeSelectionStatus =
|
|
material.composeSelection === null
|
|
? 'existing'
|
|
: publishExactFile(
|
|
material.paths.composeSelection,
|
|
material.composeSelection,
|
|
0o600,
|
|
identity.uid,
|
|
'active adopted compose selection',
|
|
);
|
|
const receiptStatus = publishExactFile(
|
|
material.paths.bundleReceipt,
|
|
material.receiptContents,
|
|
0o600,
|
|
identity.uid,
|
|
'adopted bundle receipt',
|
|
);
|
|
const createdDirectories = directoryStatuses.filter(
|
|
(status) => status === 'prepared',
|
|
).length;
|
|
const prepared =
|
|
createdDirectories > 0 ||
|
|
applicationStatus === 'prepared' ||
|
|
serviceStatus === 'prepared' ||
|
|
composeRevisionStatus === 'prepared' ||
|
|
composeSelectionStatus === 'prepared' ||
|
|
receiptStatus === 'prepared';
|
|
return Object.freeze({
|
|
schemaVersion: 1 as const,
|
|
operation: 'local.deployment.adopted.prepare' as const,
|
|
status: prepared ? ('prepared' as const) : ('existing' as const),
|
|
bundleId: command.request.bundleId,
|
|
bundleDigest: material.receipt.bundleDigest,
|
|
profile: command.options.profile,
|
|
service: Object.freeze({
|
|
kind: command.options.service.kind,
|
|
status: serviceStatus,
|
|
}),
|
|
applicationConfiguration: Object.freeze({
|
|
schema: 'qinglong/local-application-process@v4' as const,
|
|
status: applicationStatus,
|
|
}),
|
|
directories: Object.freeze({
|
|
created: createdDirectories,
|
|
existing: directoryStatuses.length - createdDirectories,
|
|
}),
|
|
});
|
|
}
|
|
|
|
export function verifyLocalDeploymentAdoptedBundle(
|
|
input: unknown,
|
|
): Readonly<LocalDeploymentAdoptedBundleResult> {
|
|
const command = normalizeLocalDeploymentAdoptedBundleCommand(
|
|
input,
|
|
'local.deployment.adopted.verify',
|
|
);
|
|
const identity = currentIdentity();
|
|
const evidence = verifyLocalDeploymentAdoptedEvidence(
|
|
command,
|
|
identity.uid,
|
|
identity.gid,
|
|
);
|
|
const material = renderLocalDeploymentAdoptedBundleMaterial(
|
|
command,
|
|
evidence,
|
|
identity.uid,
|
|
identity.gid,
|
|
);
|
|
validateExistingPrerequisites(
|
|
command.options.deploymentRoot,
|
|
material.paths.service,
|
|
material.paths.ownerPepperKeyring,
|
|
material.paths.ownerPepperBackup,
|
|
material.paths.secretKeyring,
|
|
path.dirname(command.request.cutover.commitmentPath),
|
|
identity.uid,
|
|
identity.gid,
|
|
);
|
|
rejectAlternateDescriptors(material.paths.descriptor, material.paths.service);
|
|
for (const [directory, label] of [
|
|
[material.paths.receipts, 'receiptRoot'],
|
|
[material.paths.artifacts, 'artifactRoot'],
|
|
[material.paths.pluginStaging, 'pluginStagingRoot'],
|
|
[material.paths.pluginActivation, 'pluginActivationRoot'],
|
|
...(material.composeSelection === null
|
|
? []
|
|
: ([
|
|
[material.paths.composeRevisions, 'composeRevisionRoot'],
|
|
[material.paths.composeRollouts, 'composeRolloutRoot'],
|
|
[material.paths.composeRolloutBackups, 'composeRolloutBackupRoot'],
|
|
[material.paths.composeRestores, 'composeRestoreRoot'],
|
|
[
|
|
material.paths.composeRestoreSafeguards,
|
|
'composeRestoreSafeguardRoot',
|
|
],
|
|
[
|
|
material.paths.composeEvidenceCollections,
|
|
'composeEvidenceCollectionRoot',
|
|
],
|
|
[
|
|
material.paths.composeCollectedEvidence,
|
|
'composeCollectedEvidenceRoot',
|
|
],
|
|
[
|
|
material.paths.composeCollectedRolloutBackups,
|
|
'composeCollectedRolloutBackupRoot',
|
|
],
|
|
[
|
|
material.paths.composeCollectedRestoreSafeguards,
|
|
'composeCollectedRestoreSafeguardRoot',
|
|
],
|
|
] as const)),
|
|
] as const) {
|
|
validatePrivateDirectory(directory, identity.uid, label);
|
|
}
|
|
verifyPublishedFile(
|
|
material.paths.applicationConfig,
|
|
material.applicationConfig,
|
|
0o600,
|
|
identity.uid,
|
|
identity.gid,
|
|
'adopted application configuration',
|
|
);
|
|
verifyPublishedFile(
|
|
material.paths.descriptor,
|
|
material.descriptor.contents,
|
|
material.descriptor.mode,
|
|
identity.uid,
|
|
identity.gid,
|
|
'adopted service descriptor',
|
|
);
|
|
verifyPublishedFile(
|
|
material.paths.bundleReceipt,
|
|
material.receiptContents,
|
|
0o600,
|
|
identity.uid,
|
|
identity.gid,
|
|
'adopted bundle receipt',
|
|
);
|
|
if (material.composeSelection !== null) {
|
|
verifyPublishedFile(
|
|
material.paths.composeRevision,
|
|
material.composeSelection,
|
|
0o600,
|
|
identity.uid,
|
|
identity.gid,
|
|
'initial adopted compose revision',
|
|
);
|
|
verifyPublishedFile(
|
|
material.paths.composeSelection,
|
|
material.composeSelection,
|
|
0o600,
|
|
identity.uid,
|
|
identity.gid,
|
|
'active adopted compose selection',
|
|
);
|
|
} else if (
|
|
fs.existsSync(material.paths.composeSelection) ||
|
|
fs.existsSync(material.paths.composeRevisions)
|
|
) {
|
|
configurationError('process service cannot inherit Compose material');
|
|
}
|
|
return Object.freeze({
|
|
schemaVersion: 1 as const,
|
|
operation: 'local.deployment.adopted.verify' as const,
|
|
status: 'verified' as const,
|
|
bundleId: command.request.bundleId,
|
|
bundleDigest: material.receipt.bundleDigest,
|
|
profile: command.options.profile,
|
|
service: Object.freeze({
|
|
kind: command.options.service.kind,
|
|
status: 'verified' as const,
|
|
}),
|
|
applicationConfiguration: Object.freeze({
|
|
schema: 'qinglong/local-application-process@v4' as const,
|
|
status: 'verified' as const,
|
|
}),
|
|
directories: Object.freeze({
|
|
created: 0,
|
|
existing: material.composeSelection === null ? 4 : 13,
|
|
}),
|
|
});
|
|
}
|
|
|
|
export function runLocalDeploymentAdoptedBundleCommandFile(
|
|
filePath: string,
|
|
operation: LocalDeploymentAdoptedBundleOperation,
|
|
): Readonly<LocalDeploymentAdoptedBundleResult> {
|
|
const input = readPrivateLocalCommandFile(filePath);
|
|
return operation === 'local.deployment.adopted.prepare'
|
|
? prepareLocalDeploymentAdoptedBundle(input)
|
|
: verifyLocalDeploymentAdoptedBundle(input);
|
|
}
|
|
|
|
export type { LocalDeploymentAdoptedBundleCommand };
|