mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 10:32:40 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,94 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
import { LocalDeploymentConfigurationError } from './contract';
|
||||
|
||||
const MAX_DOCKER_OUTPUT_BYTES = 256 * 1024;
|
||||
|
||||
export interface LocalDeploymentDockerRequest {
|
||||
readonly executable: string;
|
||||
readonly socketPath: string;
|
||||
readonly args: readonly string[];
|
||||
readonly timeoutMs?: number;
|
||||
}
|
||||
|
||||
export type LocalDeploymentDockerRunner = (
|
||||
request: Readonly<LocalDeploymentDockerRequest>,
|
||||
) => string;
|
||||
|
||||
export function runLocalDeploymentDockerCommand(
|
||||
request: Readonly<LocalDeploymentDockerRequest>,
|
||||
): string {
|
||||
const configRoot = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-docker-command-')),
|
||||
);
|
||||
fs.chmodSync(configRoot, 0o700);
|
||||
try {
|
||||
const executableDirectory = path.dirname(request.executable);
|
||||
const result = spawnSync(
|
||||
request.executable,
|
||||
[
|
||||
'--host',
|
||||
`unix://${request.socketPath}`,
|
||||
'--config',
|
||||
configRoot,
|
||||
...request.args,
|
||||
],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
maxBuffer: MAX_DOCKER_OUTPUT_BYTES,
|
||||
timeout: request.timeoutMs ?? 30_000,
|
||||
killSignal: 'SIGKILL',
|
||||
env: {
|
||||
PATH: `${executableDirectory}:/usr/local/bin:/usr/bin:/bin`,
|
||||
HOME: configRoot,
|
||||
DOCKER_CONFIG: configRoot,
|
||||
NO_PROXY: '*',
|
||||
no_proxy: '*',
|
||||
},
|
||||
},
|
||||
);
|
||||
if (
|
||||
result.error ||
|
||||
result.status !== 0 ||
|
||||
typeof result.stdout !== 'string' ||
|
||||
Buffer.byteLength(result.stdout, 'utf8') > MAX_DOCKER_OUTPUT_BYTES
|
||||
) {
|
||||
throw new LocalDeploymentConfigurationError(
|
||||
'Docker command failed closed',
|
||||
{ cause: result.error },
|
||||
);
|
||||
}
|
||||
return result.stdout;
|
||||
} finally {
|
||||
fs.rmSync(configRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function validateLocalDeploymentDockerSocket(
|
||||
socketPath: string,
|
||||
uid: number,
|
||||
): void {
|
||||
let stat: fs.Stats;
|
||||
try {
|
||||
stat = fs.lstatSync(socketPath);
|
||||
} catch (error) {
|
||||
throw new LocalDeploymentConfigurationError(
|
||||
'dockerSocketPath is unavailable',
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
if (
|
||||
!stat.isSocket() ||
|
||||
stat.isSymbolicLink() ||
|
||||
fs.realpathSync(socketPath) !== socketPath ||
|
||||
(stat.uid !== 0 && stat.uid !== uid) ||
|
||||
(stat.mode & 0o002) !== 0
|
||||
) {
|
||||
throw new LocalDeploymentConfigurationError(
|
||||
'dockerSocketPath must be a canonical trusted Unix socket',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { LocalDeploymentConfigurationError } from './contract';
|
||||
|
||||
const MAX_PUBLISHED_FILE_BYTES = 64 * 1024;
|
||||
|
||||
export function validatePrivateDirectory(
|
||||
directory: string,
|
||||
uid: number,
|
||||
label: string,
|
||||
): void {
|
||||
let stat: fs.Stats;
|
||||
try {
|
||||
stat = fs.lstatSync(directory);
|
||||
} catch (error) {
|
||||
throw new LocalDeploymentConfigurationError(`${label} is unavailable`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
if (
|
||||
!stat.isDirectory() ||
|
||||
stat.isSymbolicLink() ||
|
||||
stat.uid !== uid ||
|
||||
(stat.mode & 0o777) !== 0o700 ||
|
||||
fs.realpathSync(directory) !== directory
|
||||
) {
|
||||
throw new LocalDeploymentConfigurationError(
|
||||
`${label} must be a canonical current-UID 0700 directory`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function ensurePrivateDirectory(
|
||||
directory: string,
|
||||
uid: number,
|
||||
label: string,
|
||||
): 'prepared' | 'existing' {
|
||||
let created = false;
|
||||
try {
|
||||
fs.mkdirSync(directory, { mode: 0o700 });
|
||||
created = true;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
|
||||
throw new LocalDeploymentConfigurationError(
|
||||
`${label} cannot be created`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
}
|
||||
validatePrivateDirectory(directory, uid, label);
|
||||
return created ? 'prepared' : 'existing';
|
||||
}
|
||||
|
||||
function boundedBytes(contents: string, label: string): Buffer {
|
||||
const bytes = Buffer.from(contents, 'utf8');
|
||||
if (bytes.byteLength < 2 || bytes.byteLength > MAX_PUBLISHED_FILE_BYTES) {
|
||||
throw new LocalDeploymentConfigurationError(`${label} has an invalid size`);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function fileStat(
|
||||
filePath: string,
|
||||
bytes: Buffer,
|
||||
mode: number,
|
||||
uid: number,
|
||||
allowedLinks: readonly number[],
|
||||
label: string,
|
||||
): fs.Stats {
|
||||
let stat: fs.Stats;
|
||||
try {
|
||||
stat = fs.lstatSync(filePath);
|
||||
} catch (error) {
|
||||
throw new LocalDeploymentConfigurationError(`${label} is unavailable`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
if (
|
||||
!stat.isFile() ||
|
||||
stat.isSymbolicLink() ||
|
||||
stat.uid !== uid ||
|
||||
(stat.mode & 0o777) !== mode ||
|
||||
!allowedLinks.includes(stat.nlink) ||
|
||||
stat.size !== bytes.byteLength
|
||||
) {
|
||||
throw new LocalDeploymentConfigurationError(`${label} identity is invalid`);
|
||||
}
|
||||
const actual = fs.readFileSync(filePath);
|
||||
if (!bytes.equals(actual)) {
|
||||
throw new LocalDeploymentConfigurationError(`${label} content drifted`);
|
||||
}
|
||||
return stat;
|
||||
}
|
||||
|
||||
function stagePathFor(targetPath: string): string {
|
||||
return path.join(
|
||||
path.dirname(targetPath),
|
||||
`.${path.basename(targetPath)}.ql3-deploy-stage`,
|
||||
);
|
||||
}
|
||||
|
||||
export function preflightPublishedFile(
|
||||
targetPath: string,
|
||||
contents: string,
|
||||
mode: number,
|
||||
uid: number,
|
||||
label: string,
|
||||
): void {
|
||||
const bytes = boundedBytes(contents, label);
|
||||
const stagePath = stagePathFor(targetPath);
|
||||
const targetExists = fs.existsSync(targetPath);
|
||||
const stageExists = fs.existsSync(stagePath);
|
||||
const targetStat = targetExists
|
||||
? fileStat(targetPath, bytes, mode, uid, [1, 2], label)
|
||||
: null;
|
||||
const stageStat = stageExists
|
||||
? fileStat(stagePath, bytes, mode, uid, [1, 2], `${label} stage`)
|
||||
: null;
|
||||
if (
|
||||
(targetStat?.nlink === 2 || stageStat?.nlink === 2) &&
|
||||
(!targetStat ||
|
||||
!stageStat ||
|
||||
targetStat.dev !== stageStat.dev ||
|
||||
targetStat.ino !== stageStat.ino)
|
||||
) {
|
||||
throw new LocalDeploymentConfigurationError(
|
||||
`${label} stage identity drifted`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function fsyncDirectory(directory: string): void {
|
||||
const descriptor = fs.openSync(directory, fs.constants.O_RDONLY);
|
||||
try {
|
||||
fs.fsyncSync(descriptor);
|
||||
} finally {
|
||||
fs.closeSync(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
function writeStage(
|
||||
stagePath: string,
|
||||
bytes: Buffer,
|
||||
mode: number,
|
||||
uid: number,
|
||||
label: string,
|
||||
): void {
|
||||
let descriptor: number | undefined;
|
||||
let created = false;
|
||||
try {
|
||||
descriptor = fs.openSync(
|
||||
stagePath,
|
||||
fs.constants.O_WRONLY |
|
||||
fs.constants.O_CREAT |
|
||||
fs.constants.O_EXCL |
|
||||
fs.constants.O_NOFOLLOW,
|
||||
mode,
|
||||
);
|
||||
created = true;
|
||||
fs.fchmodSync(descriptor, mode);
|
||||
const stat = fs.fstatSync(descriptor);
|
||||
if (!stat.isFile() || stat.uid !== uid || stat.nlink !== 1) {
|
||||
throw new LocalDeploymentConfigurationError(
|
||||
`${label} stage identity is invalid`,
|
||||
);
|
||||
}
|
||||
let offset = 0;
|
||||
while (offset < bytes.byteLength) {
|
||||
const written = fs.writeSync(
|
||||
descriptor,
|
||||
bytes,
|
||||
offset,
|
||||
bytes.byteLength - offset,
|
||||
);
|
||||
if (written < 1) {
|
||||
throw new LocalDeploymentConfigurationError(
|
||||
`${label} stage write stalled`,
|
||||
);
|
||||
}
|
||||
offset += written;
|
||||
}
|
||||
fs.fsyncSync(descriptor);
|
||||
} catch (error) {
|
||||
if (created) {
|
||||
try {
|
||||
fs.unlinkSync(stagePath);
|
||||
} catch {
|
||||
// A failed cleanup leaves a deterministic fail-closed stage.
|
||||
}
|
||||
}
|
||||
if (error instanceof LocalDeploymentConfigurationError) throw error;
|
||||
throw new LocalDeploymentConfigurationError(
|
||||
`${label} stage cannot be written`,
|
||||
{ cause: error },
|
||||
);
|
||||
} finally {
|
||||
if (descriptor !== undefined) fs.closeSync(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
export function publishExactFile(
|
||||
targetPath: string,
|
||||
contents: string,
|
||||
mode: number,
|
||||
uid: number,
|
||||
label: string,
|
||||
): 'prepared' | 'existing' {
|
||||
const bytes = boundedBytes(contents, label);
|
||||
const directory = path.dirname(targetPath);
|
||||
const stagePath = stagePathFor(targetPath);
|
||||
preflightPublishedFile(targetPath, contents, mode, uid, label);
|
||||
const existed = fs.existsSync(targetPath);
|
||||
if (!fs.existsSync(stagePath) && !existed) {
|
||||
writeStage(stagePath, bytes, mode, uid, label);
|
||||
}
|
||||
if (!fs.existsSync(targetPath)) {
|
||||
try {
|
||||
fs.linkSync(stagePath, targetPath);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
|
||||
throw new LocalDeploymentConfigurationError(
|
||||
`${label} cannot be published`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
}
|
||||
fsyncDirectory(directory);
|
||||
}
|
||||
fileStat(targetPath, bytes, mode, uid, [1, 2], label);
|
||||
if (fs.existsSync(stagePath)) {
|
||||
const targetStat = fs.lstatSync(targetPath);
|
||||
const stageStat = fileStat(
|
||||
stagePath,
|
||||
bytes,
|
||||
mode,
|
||||
uid,
|
||||
[1, 2],
|
||||
`${label} stage`,
|
||||
);
|
||||
if (
|
||||
stageStat.nlink === 2 &&
|
||||
(targetStat.dev !== stageStat.dev || targetStat.ino !== stageStat.ino)
|
||||
) {
|
||||
throw new LocalDeploymentConfigurationError(
|
||||
`${label} stage identity drifted`,
|
||||
);
|
||||
}
|
||||
fs.unlinkSync(stagePath);
|
||||
fsyncDirectory(directory);
|
||||
}
|
||||
fileStat(targetPath, bytes, mode, uid, [1], label);
|
||||
return existed ? 'existing' : 'prepared';
|
||||
}
|
||||
|
||||
export function replaceExactFile(
|
||||
targetPath: string,
|
||||
expectedContents: string,
|
||||
nextContents: string,
|
||||
mode: number,
|
||||
uid: number,
|
||||
label: string,
|
||||
): 'prepared' | 'existing' {
|
||||
const expectedBytes = boundedBytes(expectedContents, `${label} expected`);
|
||||
const nextBytes = boundedBytes(nextContents, `${label} next`);
|
||||
if (expectedBytes.equals(nextBytes)) {
|
||||
throw new LocalDeploymentConfigurationError(
|
||||
`${label} replacement must change content`,
|
||||
);
|
||||
}
|
||||
const directory = path.dirname(targetPath);
|
||||
const stagePath = stagePathFor(targetPath);
|
||||
let targetBytes: Buffer;
|
||||
try {
|
||||
targetBytes = fs.readFileSync(targetPath);
|
||||
} catch (error) {
|
||||
throw new LocalDeploymentConfigurationError(`${label} is unavailable`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
if (targetBytes.equals(nextBytes)) {
|
||||
fileStat(targetPath, nextBytes, mode, uid, [1], label);
|
||||
if (fs.existsSync(stagePath)) {
|
||||
fileStat(stagePath, nextBytes, mode, uid, [1], `${label} stage`);
|
||||
fs.unlinkSync(stagePath);
|
||||
fsyncDirectory(directory);
|
||||
}
|
||||
return 'existing';
|
||||
}
|
||||
if (!targetBytes.equals(expectedBytes)) {
|
||||
throw new LocalDeploymentConfigurationError(
|
||||
`${label} content does not match the expected revision`,
|
||||
);
|
||||
}
|
||||
fileStat(targetPath, expectedBytes, mode, uid, [1], label);
|
||||
if (fs.existsSync(stagePath)) {
|
||||
fileStat(stagePath, nextBytes, mode, uid, [1], `${label} stage`);
|
||||
} else {
|
||||
writeStage(stagePath, nextBytes, mode, uid, label);
|
||||
}
|
||||
fileStat(targetPath, expectedBytes, mode, uid, [1], label);
|
||||
fs.renameSync(stagePath, targetPath);
|
||||
fsyncDirectory(directory);
|
||||
fileStat(targetPath, nextBytes, mode, uid, [1], label);
|
||||
return 'prepared';
|
||||
}
|
||||
|
||||
export function syncPublishedDirectory(directory: string): void {
|
||||
fsyncDirectory(directory);
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
import crypto from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
|
||||
import type { LocalSetupCommand } from '../../lifecycle/localSetup';
|
||||
import type {
|
||||
LocalDeploymentPrepareCommand,
|
||||
LocalDeploymentProcessService,
|
||||
} from './contract';
|
||||
|
||||
const CONTAINER_ROOT = '/var/lib/qinglong3';
|
||||
|
||||
export function composeProjectName(instanceId: string): string {
|
||||
const slug = instanceId.replaceAll('.', '-').slice(0, 32);
|
||||
const suffix = crypto
|
||||
.createHash('sha256')
|
||||
.update('qinglong:local-compose-project:v1\0', 'utf8')
|
||||
.update(instanceId, 'utf8')
|
||||
.digest('hex')
|
||||
.slice(0, 12);
|
||||
return `ql3-${slug}-${suffix}`;
|
||||
}
|
||||
|
||||
export interface LocalDeploymentPaths {
|
||||
readonly database: string;
|
||||
readonly ownerPepperKeyring: string;
|
||||
readonly ownerPepperBackup: string;
|
||||
readonly localSecretKeyring: string;
|
||||
readonly receipts: string;
|
||||
readonly artifacts: string;
|
||||
readonly pluginStaging: string;
|
||||
readonly pluginActivation: string;
|
||||
readonly service: string;
|
||||
readonly applicationConfig: string;
|
||||
readonly composeSelection: string;
|
||||
readonly composeRevisions: string;
|
||||
readonly composeRevisionLock: string;
|
||||
readonly composeRollouts: string;
|
||||
readonly composeRolloutLock: string;
|
||||
readonly composeRolloutBackups: string;
|
||||
readonly composeRestores: string;
|
||||
readonly composeRestoreLock: string;
|
||||
readonly composeRestoreSafeguards: string;
|
||||
readonly composeEvidenceCollections: string;
|
||||
readonly composeEvidenceCollectionLock: string;
|
||||
readonly composeCollectedEvidence: string;
|
||||
readonly composeCollectedRolloutBackups: string;
|
||||
readonly composeCollectedRestoreSafeguards: string;
|
||||
}
|
||||
|
||||
export function deploymentPaths(root: string): Readonly<LocalDeploymentPaths> {
|
||||
return Object.freeze({
|
||||
database: path.join(root, 'qinglong3.sqlite'),
|
||||
ownerPepperKeyring: path.join(root, 'owner-peppers'),
|
||||
ownerPepperBackup: path.join(root, 'owner-pepper-backup'),
|
||||
localSecretKeyring: path.join(root, 'local-secret-keyring.json'),
|
||||
receipts: path.join(root, 'receipts'),
|
||||
artifacts: path.join(root, 'artifacts'),
|
||||
pluginStaging: path.join(root, 'plugin-staging'),
|
||||
pluginActivation: path.join(root, 'plugin-activation'),
|
||||
service: path.join(root, 'service'),
|
||||
applicationConfig: path.join(root, 'local-application.json'),
|
||||
composeSelection: path.join(root, 'service', 'compose.image.yaml'),
|
||||
composeRevisions: path.join(root, 'service', 'revisions'),
|
||||
composeRevisionLock: path.join(root, 'service', '.compose-revision.lock'),
|
||||
composeRollouts: path.join(root, 'service', 'rollouts'),
|
||||
composeRolloutLock: path.join(root, 'service', '.compose-rollout.lock'),
|
||||
composeRolloutBackups: path.join(root, 'service', 'rollout-backups'),
|
||||
composeRestores: path.join(root, 'service', 'restores'),
|
||||
composeRestoreLock: path.join(root, 'service', '.compose-restore.lock'),
|
||||
composeRestoreSafeguards: path.join(root, 'service', 'restore-safeguards'),
|
||||
composeEvidenceCollections: path.join(
|
||||
root,
|
||||
'service',
|
||||
'evidence-collections',
|
||||
),
|
||||
composeEvidenceCollectionLock: path.join(
|
||||
root,
|
||||
'service',
|
||||
'.compose-evidence-collection.lock',
|
||||
),
|
||||
composeCollectedEvidence: path.join(root, 'service', 'collected-evidence'),
|
||||
composeCollectedRolloutBackups: path.join(
|
||||
root,
|
||||
'service',
|
||||
'collected-evidence',
|
||||
'rollout-backups',
|
||||
),
|
||||
composeCollectedRestoreSafeguards: path.join(
|
||||
root,
|
||||
'service',
|
||||
'collected-evidence',
|
||||
'restore-safeguards',
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export function applicationConfiguration(
|
||||
command: Readonly<LocalDeploymentPrepareCommand>,
|
||||
paths: Readonly<LocalDeploymentPaths>,
|
||||
): string {
|
||||
const runtimeRoot =
|
||||
command.options.service.kind === 'compose'
|
||||
? CONTAINER_ROOT
|
||||
: command.options.deploymentRoot;
|
||||
const pageSize = command.options.profile === 'edge' ? 4 : 16;
|
||||
return `${JSON.stringify(
|
||||
{
|
||||
schema: 'qinglong/local-application-process@v2',
|
||||
instanceId: command.options.instanceId,
|
||||
profile: command.options.profile,
|
||||
storage: {
|
||||
mode: 'fresh',
|
||||
databasePath: path.join(runtimeRoot, path.basename(paths.database)),
|
||||
...(command.options.busyTimeoutMs === undefined
|
||||
? {}
|
||||
: { busyTimeoutMs: command.options.busyTimeoutMs }),
|
||||
},
|
||||
runtime: {
|
||||
receiptRoot: path.join(runtimeRoot, path.basename(paths.receipts)),
|
||||
artifactRoot: path.join(runtimeRoot, path.basename(paths.artifacts)),
|
||||
secretKeyringPath: path.join(
|
||||
runtimeRoot,
|
||||
path.basename(paths.localSecretKeyring),
|
||||
),
|
||||
},
|
||||
pluginPackages: {
|
||||
stagingRoot: path.join(runtimeRoot, path.basename(paths.pluginStaging)),
|
||||
activationRoot: path.join(
|
||||
runtimeRoot,
|
||||
path.basename(paths.pluginActivation),
|
||||
),
|
||||
recoverySource: {
|
||||
mode: 'disabled',
|
||||
},
|
||||
pageSize,
|
||||
maxPages: pageSize,
|
||||
taskPublicationPageSize: pageSize,
|
||||
taskPublicationMaxPages: pageSize,
|
||||
},
|
||||
ai: {
|
||||
deployment: 'excluded',
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
}
|
||||
|
||||
function systemdDescriptor(
|
||||
command: Readonly<LocalDeploymentPrepareCommand>,
|
||||
configPath: string,
|
||||
uid: number,
|
||||
gid: number,
|
||||
): string {
|
||||
const service = command.options.service as LocalDeploymentProcessService;
|
||||
const edge = command.options.profile === 'edge';
|
||||
return [
|
||||
'[Unit]',
|
||||
'Description=QingLong 3.0 local automation runtime',
|
||||
'After=local-fs.target',
|
||||
'',
|
||||
'[Service]',
|
||||
'Type=simple',
|
||||
`User=${uid}`,
|
||||
`Group=${gid}`,
|
||||
`WorkingDirectory=${command.options.deploymentRoot}`,
|
||||
`ExecStart=${service.nodeExecutable} ${service.applicationEntrypoint} --config ${configPath}`,
|
||||
'Environment=NODE_ENV=production',
|
||||
'UMask=0077',
|
||||
'KillSignal=SIGTERM',
|
||||
'TimeoutStopSec=30s',
|
||||
'Restart=on-failure',
|
||||
'RestartSec=5s',
|
||||
'RestartPreventExitStatus=64',
|
||||
'NoNewPrivileges=yes',
|
||||
'PrivateTmp=yes',
|
||||
'ProtectSystem=strict',
|
||||
`ReadWritePaths=${command.options.deploymentRoot}`,
|
||||
'ProtectKernelTunables=yes',
|
||||
'ProtectKernelModules=yes',
|
||||
'ProtectControlGroups=yes',
|
||||
'RestrictSUIDSGID=yes',
|
||||
'LockPersonality=yes',
|
||||
`LimitNOFILE=${edge ? 1024 : 4096}`,
|
||||
`TasksMax=${edge ? 64 : 256}`,
|
||||
`MemoryMax=${edge ? '128M' : '256M'}`,
|
||||
'',
|
||||
'[Install]',
|
||||
'WantedBy=multi-user.target',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function openrcDescriptor(
|
||||
command: Readonly<LocalDeploymentPrepareCommand>,
|
||||
configPath: string,
|
||||
uid: number,
|
||||
gid: number,
|
||||
): string {
|
||||
const service = command.options.service as LocalDeploymentProcessService;
|
||||
const edge = command.options.profile === 'edge';
|
||||
return [
|
||||
'#!/sbin/openrc-run',
|
||||
'',
|
||||
'name="qinglong3"',
|
||||
'description="QingLong 3.0 local automation runtime"',
|
||||
`command="${service.nodeExecutable}"`,
|
||||
`command_args="${service.applicationEntrypoint} --config ${configPath}"`,
|
||||
`command_user="${uid}:${gid}"`,
|
||||
`directory="${command.options.deploymentRoot}"`,
|
||||
'supervisor="supervise-daemon"',
|
||||
'respawn_delay=5',
|
||||
'respawn_max=5',
|
||||
'respawn_period=60',
|
||||
'retry="TERM/30/KILL/5"',
|
||||
'umask=0077',
|
||||
`rc_ulimit="-n ${edge ? 1024 : 4096}"`,
|
||||
'',
|
||||
'depend() {',
|
||||
' need localmount',
|
||||
' after bootmisc',
|
||||
'}',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function composeDescriptor(
|
||||
command: Readonly<LocalDeploymentPrepareCommand>,
|
||||
uid: number,
|
||||
gid: number,
|
||||
): string {
|
||||
const edge = command.options.profile === 'edge';
|
||||
return [
|
||||
`name: ${composeProjectName(command.options.instanceId)}`,
|
||||
'',
|
||||
'services:',
|
||||
' qinglong3:',
|
||||
` user: "${uid}:${gid}"`,
|
||||
' read_only: true',
|
||||
' init: true',
|
||||
' network_mode: none',
|
||||
' command:',
|
||||
' - --config',
|
||||
` - ${CONTAINER_ROOT}/local-application.json`,
|
||||
' volumes:',
|
||||
' - type: bind',
|
||||
` source: ${command.options.deploymentRoot}`,
|
||||
` target: ${CONTAINER_ROOT}`,
|
||||
' tmpfs:',
|
||||
' - /tmp:rw,noexec,nosuid,nodev,size=16m',
|
||||
' cap_drop:',
|
||||
' - ALL',
|
||||
' security_opt:',
|
||||
' - no-new-privileges:true',
|
||||
' restart: unless-stopped',
|
||||
' stop_grace_period: 30s',
|
||||
` mem_limit: ${edge ? '128m' : '256m'}`,
|
||||
` pids_limit: ${edge ? 64 : 256}`,
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function descriptor(
|
||||
command: Readonly<LocalDeploymentPrepareCommand>,
|
||||
configPath: string,
|
||||
uid: number,
|
||||
gid: number,
|
||||
): Readonly<{ fileName: string; contents: string; mode: number }> {
|
||||
if (command.options.service.kind === 'systemd') {
|
||||
return Object.freeze({
|
||||
fileName: 'qinglong3.service',
|
||||
contents: systemdDescriptor(command, configPath, uid, gid),
|
||||
mode: 0o600,
|
||||
});
|
||||
}
|
||||
if (command.options.service.kind === 'openrc') {
|
||||
return Object.freeze({
|
||||
fileName: 'qinglong3.openrc',
|
||||
contents: openrcDescriptor(command, configPath, uid, gid),
|
||||
mode: 0o700,
|
||||
});
|
||||
}
|
||||
return Object.freeze({
|
||||
fileName: 'compose.yaml',
|
||||
contents: composeDescriptor(command, uid, gid),
|
||||
mode: 0o600,
|
||||
});
|
||||
}
|
||||
|
||||
export function setupCommand(
|
||||
command: Readonly<LocalDeploymentPrepareCommand>,
|
||||
paths: Readonly<LocalDeploymentPaths>,
|
||||
): Readonly<LocalSetupCommand> {
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
operation: 'local.setup.prepare' as const,
|
||||
options: Object.freeze({
|
||||
deploymentRoot: command.options.deploymentRoot,
|
||||
databasePath: paths.database,
|
||||
profile: command.options.profile,
|
||||
ownerPepperKeyringDirectory: paths.ownerPepperKeyring,
|
||||
ownerPepperBackupDirectory: paths.ownerPepperBackup,
|
||||
ownerPepperKeyId: command.request.ownerPepperKeyId,
|
||||
localSecretKeyringPath: paths.localSecretKeyring,
|
||||
...(command.options.busyTimeoutMs === undefined
|
||||
? {}
|
||||
: { busyTimeoutMs: command.options.busyTimeoutMs }),
|
||||
}),
|
||||
request: Object.freeze({
|
||||
registerMutationId: command.request.registerMutationId,
|
||||
activateMutationId: command.request.activateMutationId,
|
||||
registeredAtMs: command.request.registeredAtMs,
|
||||
activatedAtMs: command.request.activatedAtMs,
|
||||
}),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user