mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): ship selectable local console trial kit
This commit is contained in:
@@ -12,7 +12,7 @@ const { sha256File } = require('./ql3-local-alpha-trial-kit-bundle.cjs');
|
||||
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
||||
|
||||
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
||||
const SCHEMA = 'qinglong/alpha-stage-index@v1';
|
||||
const SCHEMA = 'qinglong/alpha-stage-index@v2';
|
||||
const FILES = Object.freeze({
|
||||
readme: 'README.md',
|
||||
manifest: 'manifest.json',
|
||||
@@ -118,8 +118,10 @@ function stageArtifactName(sourceRevision) {
|
||||
return `ql3-alpha-${sourceRevision}-stage-index`;
|
||||
}
|
||||
|
||||
function milestoneArtifactName(sourceRevision, product) {
|
||||
return `ql3-alpha-${sourceRevision}-${product}-milestone`;
|
||||
function milestoneArtifactName(sourceRevision, product, variant) {
|
||||
return product === 'local'
|
||||
? `ql3-alpha-${sourceRevision}-local-${variant}-milestone`
|
||||
: `ql3-alpha-${sourceRevision}-cluster-milestone`;
|
||||
}
|
||||
|
||||
function validateWorkflow(document, sourceRevision) {
|
||||
@@ -189,8 +191,15 @@ function readMilestones(localMilestoneRoot, clusterMilestoneRoot) {
|
||||
function expectedSelections(local, cluster) {
|
||||
return {
|
||||
local: {
|
||||
profiles: ['edge', 'standalone'],
|
||||
intent: 'fresh_non_production_trial',
|
||||
variant: local.variant,
|
||||
profiles:
|
||||
local.variant === 'console'
|
||||
? ['edge-application-api', 'standalone-application-api']
|
||||
: ['edge', 'standalone'],
|
||||
intent:
|
||||
local.variant === 'console'
|
||||
? 'fresh_loopback_console_non_production_trial'
|
||||
: 'fresh_non_production_trial',
|
||||
architectures: Object.fromEntries(
|
||||
ARCHITECTURES.map((architecture) => [
|
||||
architecture,
|
||||
@@ -224,18 +233,19 @@ function expectedSelections(local, cluster) {
|
||||
};
|
||||
}
|
||||
|
||||
function validateMilestoneRecord(record, product, sourceRevision) {
|
||||
function validateMilestoneRecord(record, product, sourceRevision, variant) {
|
||||
const expectedMaturity =
|
||||
product === 'local'
|
||||
? 'alpha_candidate_not_public_release'
|
||||
: 'cluster_integration_candidate_not_public_release';
|
||||
const expectedSchema =
|
||||
product === 'local'
|
||||
? 'qinglong/alpha-local-milestone@v1'
|
||||
? 'qinglong/alpha-local-milestone@v2'
|
||||
: 'qinglong/alpha-cluster-milestone@v1';
|
||||
if (
|
||||
!exactKeys(record, ['artifactName', 'schema', 'maturity', 'manifest']) ||
|
||||
record.artifactName !== milestoneArtifactName(sourceRevision, product) ||
|
||||
record.artifactName !==
|
||||
milestoneArtifactName(sourceRevision, product, variant) ||
|
||||
record.schema !== expectedSchema ||
|
||||
record.maturity !== expectedMaturity ||
|
||||
!exactKeys(record.manifest, ['file', 'sha256', 'bytes']) ||
|
||||
@@ -293,7 +303,7 @@ function auditAlphaStageIndex(options) {
|
||||
'deploymentSelections',
|
||||
'readme',
|
||||
]) ||
|
||||
manifest.schemaVersion !== 1 ||
|
||||
manifest.schemaVersion !== 2 ||
|
||||
manifest.schema !== SCHEMA ||
|
||||
manifest.maturity !== 'alpha_stage_delivery_not_public_release' ||
|
||||
manifest.product !== 'qinglong3' ||
|
||||
@@ -321,11 +331,13 @@ function auditAlphaStageIndex(options) {
|
||||
manifest.milestones.local,
|
||||
'local',
|
||||
manifest.sourceRevision,
|
||||
milestones.local.variant,
|
||||
);
|
||||
validateMilestoneRecord(
|
||||
manifest.milestones.cluster,
|
||||
'cluster',
|
||||
manifest.sourceRevision,
|
||||
undefined,
|
||||
);
|
||||
const milestoneManifests = {
|
||||
local: fileRecord(
|
||||
@@ -365,12 +377,15 @@ function auditAlphaStageIndex(options) {
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
schema: 'qinglong/alpha-stage-index-audit@v1',
|
||||
schema: 'qinglong/alpha-stage-index-audit@v2',
|
||||
version: manifest.version,
|
||||
sourceRevision: manifest.sourceRevision,
|
||||
workflowRunId: manifest.workflow.runId,
|
||||
workflowRunAttempt: manifest.workflow.runAttempt,
|
||||
profiles: ['edge', 'standalone', 'cluster'],
|
||||
profiles: [
|
||||
...manifest.deploymentSelections.local.profiles,
|
||||
'cluster',
|
||||
],
|
||||
artifactCount: 10,
|
||||
compatible: true,
|
||||
});
|
||||
@@ -441,7 +456,7 @@ function finalizeAlphaStageIndex(options) {
|
||||
path.join(normalized.outputRoot, FILES.readme),
|
||||
);
|
||||
const manifest = {
|
||||
schemaVersion: 1,
|
||||
schemaVersion: 2,
|
||||
schema: SCHEMA,
|
||||
maturity: 'alpha_stage_delivery_not_public_release',
|
||||
product: 'qinglong3',
|
||||
@@ -461,6 +476,7 @@ function finalizeAlphaStageIndex(options) {
|
||||
artifactName: milestoneArtifactName(
|
||||
normalized.sourceRevision,
|
||||
'local',
|
||||
normalized.local.variant,
|
||||
),
|
||||
schema: normalized.local.schema,
|
||||
maturity: normalized.local.maturity,
|
||||
@@ -473,6 +489,7 @@ function finalizeAlphaStageIndex(options) {
|
||||
artifactName: milestoneArtifactName(
|
||||
normalized.sourceRevision,
|
||||
'cluster',
|
||||
undefined,
|
||||
),
|
||||
schema: normalized.cluster.schema,
|
||||
maturity: normalized.cluster.maturity,
|
||||
@@ -542,7 +559,7 @@ function auditAlphaStageIndexWorkflow(root = DEFAULT_ROOT) {
|
||||
`if: ${condition}`,
|
||||
' - local-alpha-milestone\n',
|
||||
' - cluster-alpha-milestone\n',
|
||||
`name: ql3-alpha-${'${{ github.sha }}'}-local-milestone`,
|
||||
`name: ql3-alpha-${'${{ github.sha }}'}-local-${'${{ inputs.local_alpha_variant }}'}-milestone`,
|
||||
`name: ql3-alpha-${'${{ github.sha }}'}-cluster-milestone`,
|
||||
'scripts/ql3-alpha-stage-index.cjs',
|
||||
'--mode=finalize',
|
||||
|
||||
@@ -76,6 +76,30 @@ const IMAGE_PROFILES = Object.freeze({
|
||||
'drizzle-orm': '1.0.0-rc.4',
|
||||
}),
|
||||
}),
|
||||
'local-console': Object.freeze({
|
||||
id: 'local-console',
|
||||
buildManifestPath: 'deploy/containers/ql3-local-application/package.json',
|
||||
buildLockPath: 'deploy/containers/ql3-local-application/package-lock.json',
|
||||
imageManifestPath:
|
||||
'deploy/containers/ql3-local-application/runtime-dependencies/package.json',
|
||||
imageLockPath:
|
||||
'deploy/containers/ql3-local-application/runtime-dependencies/package-lock.json',
|
||||
internalManifestPaths: Object.freeze([
|
||||
'packages/ql3-runtime-core/package.json',
|
||||
'packages/ql3-local-admin/package.json',
|
||||
'packages/ql3-local-api/package.json',
|
||||
'packages/ql3-local-application/package.json',
|
||||
'packages/ql3-local-command-file/package.json',
|
||||
'packages/ql3-local-execution/package.json',
|
||||
'packages/ql3-local-owner-console/package.json',
|
||||
'packages/ql3-local-process/package.json',
|
||||
'packages/ql3-local-secret/package.json',
|
||||
'packages/ql3-local-sqlite/package.json',
|
||||
]),
|
||||
buildOnlyDependencies: Object.freeze({
|
||||
'drizzle-orm': '1.0.0-rc.4',
|
||||
}),
|
||||
}),
|
||||
'local-operator': Object.freeze({
|
||||
id: 'local-operator',
|
||||
buildManifestPath: 'deploy/containers/ql3-local-operator/package.json',
|
||||
@@ -132,7 +156,7 @@ function resolveImageProfile(value = 'control') {
|
||||
const profile = IMAGE_PROFILES[value];
|
||||
if (!profile) {
|
||||
throw new Error(
|
||||
'image profile must be exactly control, control-ai, admin, local, local-operator or worker',
|
||||
'image profile must be exactly control, control-ai, admin, local, local-console, local-operator or worker',
|
||||
);
|
||||
}
|
||||
return profile;
|
||||
|
||||
@@ -12,6 +12,7 @@ const IMAGES = Object.freeze([
|
||||
'control',
|
||||
'control-ai',
|
||||
'local',
|
||||
'local-console',
|
||||
'local-operator',
|
||||
'worker',
|
||||
]);
|
||||
@@ -109,6 +110,7 @@ function auditImageOsVulnerabilityPolicy(
|
||||
control: 0,
|
||||
'control-ai': 0,
|
||||
local: 0,
|
||||
'local-console': 0,
|
||||
'local-operator': 0,
|
||||
worker: 0,
|
||||
}),
|
||||
@@ -120,6 +122,7 @@ function auditImageOsVulnerabilityPolicy(
|
||||
control: 0,
|
||||
'control-ai': 0,
|
||||
local: 0,
|
||||
'local-console': 0,
|
||||
'local-operator': 0,
|
||||
worker: 0,
|
||||
};
|
||||
|
||||
@@ -7,11 +7,12 @@ const path = require('node:path');
|
||||
const {
|
||||
auditLocalAlphaTrialKit,
|
||||
sha256File,
|
||||
VARIANTS,
|
||||
} = require('./ql3-local-alpha-trial-kit-bundle.cjs');
|
||||
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
||||
|
||||
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
||||
const SCHEMA = 'qinglong/alpha-local-milestone@v1';
|
||||
const SCHEMA = 'qinglong/alpha-local-milestone@v2';
|
||||
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
||||
const FILES = Object.freeze({
|
||||
readme: 'README.md',
|
||||
@@ -126,8 +127,8 @@ function checksumContents(root, names) {
|
||||
.join('\n')}\n`;
|
||||
}
|
||||
|
||||
function artifactName(sourceRevision, architecture) {
|
||||
return `ql3-alpha-${sourceRevision}-local-${architecture}`;
|
||||
function artifactName(sourceRevision, architecture, variant = 'headless') {
|
||||
return `ql3-alpha-${sourceRevision}-local-${variant}-${architecture}`;
|
||||
}
|
||||
|
||||
function validateIdentity(options) {
|
||||
@@ -150,6 +151,7 @@ function validateFinalizeOptions(options) {
|
||||
const outputRoot = path.resolve(options.outputRoot || '');
|
||||
const parent = path.dirname(outputRoot);
|
||||
if (
|
||||
!VARIANTS.includes(options.variant) ||
|
||||
!path.isAbsolute(outputRoot) ||
|
||||
fs.existsSync(outputRoot) ||
|
||||
fs.realpathSync(parent) !== parent
|
||||
@@ -179,6 +181,7 @@ function validateFinalizeOptions(options) {
|
||||
'milestone README',
|
||||
),
|
||||
sourceRevision: options.sourceRevision,
|
||||
variant: options.variant,
|
||||
repository: options.repository,
|
||||
workflowRef: options.workflowRef,
|
||||
workflowSha: options.workflowSha,
|
||||
@@ -196,12 +199,17 @@ function bundleRecord(options, architecture) {
|
||||
report.architecture !== architecture ||
|
||||
report.sourceRevision !== options.sourceRevision ||
|
||||
report.workflowRunId !== options.runId ||
|
||||
report.workflowRunAttempt !== options.runAttempt
|
||||
report.workflowRunAttempt !== options.runAttempt ||
|
||||
report.variant !== options.variant
|
||||
) {
|
||||
fail(`${architecture} trial kit is detached from the milestone run`);
|
||||
}
|
||||
return Object.freeze({
|
||||
artifactName: artifactName(options.sourceRevision, architecture),
|
||||
artifactName: artifactName(
|
||||
options.sourceRevision,
|
||||
architecture,
|
||||
options.variant,
|
||||
),
|
||||
architecture,
|
||||
bundleManifest: fileRecord(
|
||||
path.join(bundleRoot, 'manifest.json'),
|
||||
@@ -226,7 +234,7 @@ function validateArtifactRecord(record, architecture, manifest) {
|
||||
'verificationSha256',
|
||||
]) ||
|
||||
record.artifactName !==
|
||||
artifactName(manifest.sourceRevision, architecture) ||
|
||||
artifactName(manifest.sourceRevision, architecture, manifest.variant) ||
|
||||
record.architecture !== architecture ||
|
||||
!exactKeys(record.bundleManifest, ['file', 'sha256', 'bytes']) ||
|
||||
record.bundleManifest.file !== 'manifest.json' ||
|
||||
@@ -298,16 +306,18 @@ function auditLocalAlphaMilestone(options) {
|
||||
'schema',
|
||||
'maturity',
|
||||
'product',
|
||||
'variant',
|
||||
'version',
|
||||
'sourceRevision',
|
||||
'workflow',
|
||||
'artifacts',
|
||||
'readme',
|
||||
]) ||
|
||||
manifest.schemaVersion !== 1 ||
|
||||
manifest.schemaVersion !== 2 ||
|
||||
manifest.schema !== SCHEMA ||
|
||||
manifest.maturity !== 'alpha_candidate_not_public_release' ||
|
||||
manifest.product !== 'local' ||
|
||||
!VARIANTS.includes(manifest.variant) ||
|
||||
typeof manifest.version !== 'string' ||
|
||||
manifest.version.length < 3 ||
|
||||
manifest.version.length > 64 ||
|
||||
@@ -373,9 +383,10 @@ function auditLocalAlphaMilestone(options) {
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
schema: 'qinglong/alpha-local-milestone-audit@v1',
|
||||
schema: 'qinglong/alpha-local-milestone-audit@v2',
|
||||
sourceRevision: manifest.sourceRevision,
|
||||
version: manifest.version,
|
||||
variant: manifest.variant,
|
||||
workflowRunId: manifest.workflow.runId,
|
||||
workflowRunAttempt: manifest.workflow.runAttempt,
|
||||
architectures: [...ARCHITECTURES],
|
||||
@@ -422,10 +433,11 @@ function finalizeLocalAlphaMilestone(options) {
|
||||
path.join(normalized.outputRoot, FILES.readme),
|
||||
);
|
||||
const manifest = {
|
||||
schemaVersion: 1,
|
||||
schemaVersion: 2,
|
||||
schema: SCHEMA,
|
||||
maturity: 'alpha_candidate_not_public_release',
|
||||
product: 'local',
|
||||
variant: normalized.variant,
|
||||
version: [...versions][0],
|
||||
sourceRevision: normalized.sourceRevision,
|
||||
workflow: {
|
||||
@@ -492,6 +504,10 @@ function auditLocalAlphaMilestoneWorkflow(root = DEFAULT_ROOT) {
|
||||
'- local',
|
||||
'- cluster',
|
||||
'- all',
|
||||
'local_alpha_variant:',
|
||||
'default: headless',
|
||||
'- headless',
|
||||
'- console',
|
||||
"github.run_id || 'validation'",
|
||||
"cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts) }}",
|
||||
];
|
||||
@@ -512,12 +528,13 @@ function auditLocalAlphaMilestoneWorkflow(root = DEFAULT_ROOT) {
|
||||
' name: Finalize the Local Alpha milestone',
|
||||
' needs:',
|
||||
'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c',
|
||||
`name: ql3-alpha-${'${{ github.sha }}'}-local-amd64`,
|
||||
`name: ql3-alpha-${'${{ github.sha }}'}-local-arm64`,
|
||||
`name: ql3-alpha-${'${{ github.sha }}'}-local-${'${{ inputs.local_alpha_variant }}'}-amd64`,
|
||||
`name: ql3-alpha-${'${{ github.sha }}'}-local-${'${{ inputs.local_alpha_variant }}'}-arm64`,
|
||||
'scripts/ql3-local-alpha-milestone.cjs',
|
||||
'--mode=finalize',
|
||||
'--variant=${{ inputs.local_alpha_variant }}',
|
||||
'--mode=audit',
|
||||
`name: ql3-alpha-${'${{ github.sha }}'}-local-milestone`,
|
||||
`name: ql3-alpha-${'${{ github.sha }}'}-local-${'${{ inputs.local_alpha_variant }}'}-milestone`,
|
||||
'retention-days: 30',
|
||||
'overwrite: false',
|
||||
];
|
||||
@@ -587,6 +604,7 @@ function parseArguments(argv) {
|
||||
'run-attempt',
|
||||
'run-id',
|
||||
'source-revision',
|
||||
'variant',
|
||||
'workflow-ref',
|
||||
'workflow-sha',
|
||||
];
|
||||
@@ -605,6 +623,7 @@ function parseArguments(argv) {
|
||||
},
|
||||
readme: path.resolve(values.readme),
|
||||
sourceRevision: values['source-revision'],
|
||||
variant: values.variant,
|
||||
repository: values.repository,
|
||||
workflowRef: values['workflow-ref'],
|
||||
workflowSha: values['workflow-sha'],
|
||||
|
||||
@@ -10,13 +10,14 @@ const { auditClusterImageSbom } = require('./ql3-cluster-image-sbom.cjs');
|
||||
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
||||
|
||||
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
||||
const SCHEMA = 'qinglong/alpha-local-trial-kit@v3';
|
||||
const VERIFICATION_SCHEMA = 'qinglong/alpha-local-trial-kit-verification@v1';
|
||||
const SCHEMA = 'qinglong/alpha-local-trial-kit@v4';
|
||||
const VERIFICATION_SCHEMA = 'qinglong/alpha-local-trial-kit-verification@v2';
|
||||
const QUICKSTART_TEMPLATE = path.join(
|
||||
DEFAULT_ROOT,
|
||||
'scripts/templates/ql3-local-alpha-quickstart.sh',
|
||||
);
|
||||
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
||||
const VARIANTS = Object.freeze(['headless', 'console']);
|
||||
const ARCHIVE_MIN_BYTES = 1024;
|
||||
const MAX_JSON_BYTES = 4 * 1024 * 1024;
|
||||
const MAX_README_BYTES = 512 * 1024;
|
||||
@@ -43,6 +44,13 @@ const VERIFICATION = Object.freeze({
|
||||
standaloneFreshLifecycle: 'passed',
|
||||
localApiCancellation: 'passed',
|
||||
});
|
||||
|
||||
function verificationGates(variant) {
|
||||
return Object.freeze({
|
||||
...VERIFICATION,
|
||||
consoleLiveJourney: variant === 'console' ? 'passed' : 'not_applicable',
|
||||
});
|
||||
}
|
||||
const WORKFLOW_IDENTITY = Object.freeze({
|
||||
repository: 'whyour/qinglong',
|
||||
workflowRef: 'whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
|
||||
@@ -162,11 +170,13 @@ function validateImageReference(value, label) {
|
||||
}
|
||||
|
||||
function normalizeImageInspection(inspection, options) {
|
||||
const { architecture, reference, revision, role, version } = options;
|
||||
const { architecture, reference, revision, role, variant, version } = options;
|
||||
const labels = inspection?.Config?.Labels;
|
||||
const expectedTitle =
|
||||
role === 'application'
|
||||
? 'QingLong 3.0 Local Application'
|
||||
? variant === 'console'
|
||||
? 'QingLong 3.0 Local Console Application'
|
||||
: 'QingLong 3.0 Local Application'
|
||||
: 'QingLong 3.0 Local Operator';
|
||||
if (
|
||||
!SHA256_PATTERN.test(inspection?.Id || '') ||
|
||||
@@ -183,8 +193,14 @@ function normalizeImageInspection(inspection, options) {
|
||||
}
|
||||
if (
|
||||
role === 'application' &&
|
||||
(labels?.['io.qinglong.profile'] !== 'edge,standalone' ||
|
||||
labels?.['io.qinglong.ai'] !== 'excluded')
|
||||
(labels?.['io.qinglong.profile'] !==
|
||||
(variant === 'console'
|
||||
? 'edge-application-api,standalone-application-api'
|
||||
: 'edge,standalone') ||
|
||||
labels?.['io.qinglong.ai'] !== 'excluded' ||
|
||||
(variant === 'console'
|
||||
? labels?.['io.qinglong.local.console'] !== 'offline-loopback'
|
||||
: labels?.['io.qinglong.local.console'] !== undefined))
|
||||
) {
|
||||
fail('application image profile is incompatible');
|
||||
}
|
||||
@@ -259,12 +275,14 @@ function validateVerificationEvidence(document, expected) {
|
||||
'version',
|
||||
'sourceRevision',
|
||||
'architecture',
|
||||
'variant',
|
||||
'applicationImageId',
|
||||
'operatorImageId',
|
||||
]) ||
|
||||
document.subject.version !== expected.version ||
|
||||
document.subject.sourceRevision !== expected.sourceRevision ||
|
||||
document.subject.architecture !== expected.architecture ||
|
||||
document.subject.variant !== expected.variant ||
|
||||
document.subject.applicationImageId !== expected.applicationImageId ||
|
||||
document.subject.operatorImageId !== expected.operatorImageId ||
|
||||
document.subject.applicationImageId === document.subject.operatorImageId ||
|
||||
@@ -284,8 +302,9 @@ function validateVerificationEvidence(document, expected) {
|
||||
document.workflow.job !== WORKFLOW_IDENTITY.job ||
|
||||
!DECIMAL_ID_PATTERN.test(document.workflow.runId || '') ||
|
||||
!ATTEMPT_PATTERN.test(document.workflow.runAttempt || '') ||
|
||||
!exactKeys(document.gates, Object.keys(VERIFICATION)) ||
|
||||
JSON.stringify(document.gates) !== JSON.stringify(VERIFICATION)
|
||||
!exactKeys(document.gates, Object.keys(verificationGates(expected.variant))) ||
|
||||
JSON.stringify(document.gates) !==
|
||||
JSON.stringify(verificationGates(expected.variant))
|
||||
) {
|
||||
fail('trial kit verification evidence is incompatible');
|
||||
}
|
||||
@@ -298,6 +317,7 @@ function validateVerificationOptions(options) {
|
||||
const parent = path.dirname(output);
|
||||
if (
|
||||
!ARCHITECTURES.includes(options.architecture) ||
|
||||
!VARIANTS.includes(options.variant) ||
|
||||
!REVISION_PATTERN.test(options.sourceRevision || '') ||
|
||||
!path.isAbsolute(output) ||
|
||||
fs.existsSync(output) ||
|
||||
@@ -316,6 +336,7 @@ function validateVerificationOptions(options) {
|
||||
root,
|
||||
output,
|
||||
architecture: options.architecture,
|
||||
variant: options.variant,
|
||||
sourceRevision: options.sourceRevision,
|
||||
applicationImage: validateImageReference(
|
||||
options.applicationImage,
|
||||
@@ -343,6 +364,7 @@ function createLocalAlphaTrialKitVerificationEvidence(options, adapters = {}) {
|
||||
reference: normalized.applicationImage,
|
||||
revision: normalized.sourceRevision,
|
||||
role: 'application',
|
||||
variant: normalized.variant,
|
||||
version: release.version,
|
||||
},
|
||||
);
|
||||
@@ -353,6 +375,7 @@ function createLocalAlphaTrialKitVerificationEvidence(options, adapters = {}) {
|
||||
reference: normalized.operatorImage,
|
||||
revision: normalized.sourceRevision,
|
||||
role: 'operator',
|
||||
variant: normalized.variant,
|
||||
version: release.version,
|
||||
},
|
||||
);
|
||||
@@ -364,6 +387,7 @@ function createLocalAlphaTrialKitVerificationEvidence(options, adapters = {}) {
|
||||
version: release.version,
|
||||
sourceRevision: normalized.sourceRevision,
|
||||
architecture: normalized.architecture,
|
||||
variant: normalized.variant,
|
||||
applicationImageId: application.id,
|
||||
operatorImageId: operator.id,
|
||||
},
|
||||
@@ -376,15 +400,17 @@ function createLocalAlphaTrialKitVerificationEvidence(options, adapters = {}) {
|
||||
runId: normalized.runId,
|
||||
runAttempt: normalized.runAttempt,
|
||||
},
|
||||
gates: { ...VERIFICATION },
|
||||
gates: { ...verificationGates(normalized.variant) },
|
||||
};
|
||||
validateVerificationEvidence(evidence, evidence.subject);
|
||||
writeExclusive(normalized.output, `${JSON.stringify(evidence, null, 2)}\n`);
|
||||
return evidence;
|
||||
}
|
||||
|
||||
function archiveName(architecture) {
|
||||
return `qinglong3-local-trial-kit-${architecture}.docker.tar`;
|
||||
function archiveName(architecture, variant = 'headless') {
|
||||
return variant === 'console'
|
||||
? `qinglong3-local-console-trial-kit-${architecture}.docker.tar`
|
||||
: `qinglong3-local-trial-kit-${architecture}.docker.tar`;
|
||||
}
|
||||
|
||||
function renderQuickstart(identity) {
|
||||
@@ -404,6 +430,7 @@ function renderQuickstart(identity) {
|
||||
'@@ARCHITECTURE@@': identity.architecture,
|
||||
'@@SOURCE_REVISION@@': identity.sourceRevision,
|
||||
'@@ARCHIVE@@': identity.archive.file,
|
||||
'@@VARIANT@@': identity.variant,
|
||||
});
|
||||
let rendered = template;
|
||||
for (const [token, value] of Object.entries(replacements)) {
|
||||
@@ -442,6 +469,7 @@ function validateCreateOptions(options) {
|
||||
const parent = path.dirname(outputRoot);
|
||||
if (
|
||||
!ARCHITECTURES.includes(options.architecture) ||
|
||||
!VARIANTS.includes(options.variant) ||
|
||||
!REVISION_PATTERN.test(options.sourceRevision || '') ||
|
||||
!path.isAbsolute(outputRoot) ||
|
||||
fs.existsSync(outputRoot) ||
|
||||
@@ -453,6 +481,7 @@ function validateCreateOptions(options) {
|
||||
root,
|
||||
outputRoot,
|
||||
architecture: options.architecture,
|
||||
variant: options.variant,
|
||||
sourceRevision: options.sourceRevision,
|
||||
applicationImage: validateImageReference(
|
||||
options.applicationImage,
|
||||
@@ -501,7 +530,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
);
|
||||
validateSbom(applicationSbom, {
|
||||
root: normalized.root,
|
||||
profile: 'local',
|
||||
profile: normalized.variant === 'console' ? 'local-console' : 'local',
|
||||
version: release.version,
|
||||
});
|
||||
validateSbom(operatorSbom, {
|
||||
@@ -516,6 +545,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
reference: normalized.applicationImage,
|
||||
revision: normalized.sourceRevision,
|
||||
role: 'application',
|
||||
variant: normalized.variant,
|
||||
version: release.version,
|
||||
},
|
||||
);
|
||||
@@ -526,6 +556,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
reference: normalized.operatorImage,
|
||||
revision: normalized.sourceRevision,
|
||||
role: 'operator',
|
||||
variant: normalized.variant,
|
||||
version: release.version,
|
||||
},
|
||||
);
|
||||
@@ -534,6 +565,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
version: release.version,
|
||||
sourceRevision: normalized.sourceRevision,
|
||||
architecture: normalized.architecture,
|
||||
variant: normalized.variant,
|
||||
applicationImageId: application.id,
|
||||
operatorImageId: operator.id,
|
||||
});
|
||||
@@ -542,7 +574,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
try {
|
||||
fs.mkdirSync(normalized.outputRoot, { mode: 0o700 });
|
||||
created = true;
|
||||
const archive = archiveName(normalized.architecture);
|
||||
const archive = archiveName(normalized.architecture, normalized.variant);
|
||||
const archivePath = path.join(normalized.outputRoot, archive);
|
||||
saveImages(
|
||||
[normalized.applicationImage, normalized.operatorImage],
|
||||
@@ -576,6 +608,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
const manifestIdentity = {
|
||||
sourceRevision: normalized.sourceRevision,
|
||||
architecture: normalized.architecture,
|
||||
variant: normalized.variant,
|
||||
archive: { file: archive },
|
||||
images: { application, operator },
|
||||
};
|
||||
@@ -585,13 +618,14 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
0o700,
|
||||
);
|
||||
const manifest = {
|
||||
schemaVersion: 4,
|
||||
schemaVersion: 5,
|
||||
schema: SCHEMA,
|
||||
maturity: 'alpha_candidate_not_public_release',
|
||||
product: 'local',
|
||||
version: release.version,
|
||||
sourceRevision: normalized.sourceRevision,
|
||||
architecture: normalized.architecture,
|
||||
variant: normalized.variant,
|
||||
archive: fileRecord(normalized.outputRoot, archive),
|
||||
images: { application, operator },
|
||||
sboms: {
|
||||
@@ -679,6 +713,7 @@ function auditLocalAlphaTrialKit(options) {
|
||||
'version',
|
||||
'sourceRevision',
|
||||
'architecture',
|
||||
'variant',
|
||||
'archive',
|
||||
'images',
|
||||
'sboms',
|
||||
@@ -686,13 +721,14 @@ function auditLocalAlphaTrialKit(options) {
|
||||
'readme',
|
||||
'verification',
|
||||
]) ||
|
||||
manifest.schemaVersion !== 4 ||
|
||||
manifest.schemaVersion !== 5 ||
|
||||
manifest.schema !== SCHEMA ||
|
||||
manifest.maturity !== 'alpha_candidate_not_public_release' ||
|
||||
manifest.product !== 'local' ||
|
||||
typeof manifest.version !== 'string' ||
|
||||
!REVISION_PATTERN.test(manifest.sourceRevision || '') ||
|
||||
!ARCHITECTURES.includes(manifest.architecture) ||
|
||||
!VARIANTS.includes(manifest.variant) ||
|
||||
!exactKeys(manifest.images, ['application', 'operator']) ||
|
||||
!exactKeys(manifest.sboms, ['application', 'operator'])
|
||||
) {
|
||||
@@ -703,7 +739,7 @@ function auditLocalAlphaTrialKit(options) {
|
||||
if (manifest.images.application.id === manifest.images.operator.id) {
|
||||
fail('trial kit images must be distinct');
|
||||
}
|
||||
const expectedArchive = archiveName(manifest.architecture);
|
||||
const expectedArchive = archiveName(manifest.architecture, manifest.variant);
|
||||
validateFileRecord(manifest.archive, expectedArchive, bundleRoot);
|
||||
if (manifest.archive.bytes < ARCHIVE_MIN_BYTES) {
|
||||
fail('Docker archive is unexpectedly small');
|
||||
@@ -738,7 +774,7 @@ function auditLocalAlphaTrialKit(options) {
|
||||
path.join(bundleRoot, FILES.applicationSbom),
|
||||
'application SBOM',
|
||||
),
|
||||
'local',
|
||||
manifest.variant === 'console' ? 'local-console' : 'local',
|
||||
manifest.version,
|
||||
);
|
||||
validateOfflineSbom(
|
||||
@@ -755,6 +791,7 @@ function auditLocalAlphaTrialKit(options) {
|
||||
version: manifest.version,
|
||||
sourceRevision: manifest.sourceRevision,
|
||||
architecture: manifest.architecture,
|
||||
variant: manifest.variant,
|
||||
applicationImageId: manifest.images.application.id,
|
||||
operatorImageId: manifest.images.operator.id,
|
||||
},
|
||||
@@ -800,10 +837,11 @@ function auditLocalAlphaTrialKit(options) {
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
schema: 'qinglong/alpha-local-trial-kit-audit@v1',
|
||||
schema: 'qinglong/alpha-local-trial-kit-audit@v2',
|
||||
sourceRevision: manifest.sourceRevision,
|
||||
version: manifest.version,
|
||||
architecture: manifest.architecture,
|
||||
variant: manifest.variant,
|
||||
archiveSha256: manifest.archive.sha256,
|
||||
applicationImageId: manifest.images.application.id,
|
||||
operatorImageId: manifest.images.operator.id,
|
||||
@@ -845,6 +883,7 @@ function parseArguments(argv) {
|
||||
'run-attempt',
|
||||
'run-id',
|
||||
'source-revision',
|
||||
'variant',
|
||||
'workflow-ref',
|
||||
'workflow-sha',
|
||||
];
|
||||
@@ -857,6 +896,7 @@ function parseArguments(argv) {
|
||||
mode: 'record-verification',
|
||||
output: path.resolve(values.output),
|
||||
architecture: values.architecture,
|
||||
variant: values.variant,
|
||||
sourceRevision: values['source-revision'],
|
||||
applicationImage: values['application-image'],
|
||||
operatorImage: values['operator-image'],
|
||||
@@ -880,6 +920,7 @@ function parseArguments(argv) {
|
||||
'output',
|
||||
'readme',
|
||||
'source-revision',
|
||||
'variant',
|
||||
'verification-evidence',
|
||||
];
|
||||
if (
|
||||
@@ -891,6 +932,7 @@ function parseArguments(argv) {
|
||||
mode: 'create',
|
||||
outputRoot: path.resolve(values.output),
|
||||
architecture: values.architecture,
|
||||
variant: values.variant,
|
||||
sourceRevision: values['source-revision'],
|
||||
applicationImage: values['application-image'],
|
||||
operatorImage: values['operator-image'],
|
||||
@@ -933,6 +975,7 @@ module.exports = Object.freeze({
|
||||
SCHEMA,
|
||||
VERIFICATION,
|
||||
VERIFICATION_SCHEMA,
|
||||
VARIANTS,
|
||||
archiveName,
|
||||
auditLocalAlphaTrialKit,
|
||||
createLocalAlphaTrialKit,
|
||||
|
||||
@@ -20,27 +20,30 @@ function fail(message) {
|
||||
function argumentsFrom(argv) {
|
||||
const values = {};
|
||||
for (const argument of argv) {
|
||||
const match = /^--(application-image|operator-image|profile)=(.+)$/u.exec(
|
||||
argument,
|
||||
);
|
||||
const match =
|
||||
/^--(application-image|operator-image|profile|variant)=(.+)$/u.exec(
|
||||
argument,
|
||||
);
|
||||
if (!match || Object.hasOwn(values, match[1]))
|
||||
fail('arguments are invalid');
|
||||
values[match[1]] = match[2];
|
||||
}
|
||||
if (
|
||||
Object.keys(values).length !== 3 ||
|
||||
Object.keys(values).length !== 4 ||
|
||||
!IMAGE_PATTERN.test(values['application-image'] ?? '') ||
|
||||
!IMAGE_PATTERN.test(values['operator-image'] ?? '') ||
|
||||
!['edge', 'standalone'].includes(values.profile)
|
||||
!['edge', 'standalone'].includes(values.profile) ||
|
||||
!['headless', 'console'].includes(values.variant)
|
||||
) {
|
||||
fail(
|
||||
'usage: --application-image=... --operator-image=... --profile=edge|standalone',
|
||||
'usage: --application-image=... --operator-image=... --profile=edge|standalone --variant=headless|console',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
applicationImage: values['application-image'],
|
||||
operatorImage: values['operator-image'],
|
||||
profile: values.profile,
|
||||
variant: values.variant,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,41 +64,54 @@ function docker(args, options = {}) {
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
function inspectImages(applicationImage, operatorImage) {
|
||||
const application = docker([
|
||||
'image',
|
||||
'inspect',
|
||||
'--format',
|
||||
'{{.Id}} {{.Architecture}} {{.Config.User}} {{index .Config.Labels "org.opencontainers.image.revision"}} {{index .Config.Labels "org.opencontainers.image.version"}}',
|
||||
applicationImage,
|
||||
]).split(' ');
|
||||
const operator = docker([
|
||||
'image',
|
||||
'inspect',
|
||||
'--format',
|
||||
'{{.Id}} {{.Architecture}} {{.Config.User}} {{index .Config.Labels "io.qinglong.lifecycle"}} {{index .Config.Labels "io.qinglong.authority"}} {{index .Config.Labels "org.opencontainers.image.revision"}} {{index .Config.Labels "org.opencontainers.image.version"}}',
|
||||
operatorImage,
|
||||
]).split(' ');
|
||||
function inspectImages(applicationImage, operatorImage, variant) {
|
||||
const application = JSON.parse(
|
||||
docker(['image', 'inspect', applicationImage]),
|
||||
)[0];
|
||||
const operator = JSON.parse(docker(['image', 'inspect', operatorImage]))[0];
|
||||
const applicationLabels = application?.Config?.Labels;
|
||||
const operatorLabels = operator?.Config?.Labels;
|
||||
const expectedTitle =
|
||||
variant === 'console'
|
||||
? 'QingLong 3.0 Local Console Application'
|
||||
: 'QingLong 3.0 Local Application';
|
||||
const expectedProfile =
|
||||
variant === 'console'
|
||||
? 'edge-application-api,standalone-application-api'
|
||||
: 'edge,standalone';
|
||||
if (
|
||||
!/^sha256:[0-9a-f]{64}$/u.test(application[0] ?? '') ||
|
||||
!/^sha256:[0-9a-f]{64}$/u.test(operator[0] ?? '') ||
|
||||
application[1] !== operator[1] ||
|
||||
!['amd64', 'arm64'].includes(application[1]) ||
|
||||
application[2] !== '65532:65532' ||
|
||||
operator[2] !== '65532:65532' ||
|
||||
operator[3] !== 'short-lived' ||
|
||||
operator[4] !== 'local-owner-management' ||
|
||||
!/^[0-9a-f]{40}$/u.test(application[3] ?? '') ||
|
||||
application[3] !== operator[5] ||
|
||||
application[4] !== operator[6] ||
|
||||
!/^3\.0\.0-alpha\.[0-9]+$/u.test(application[4] ?? '')
|
||||
!/^sha256:[0-9a-f]{64}$/u.test(application?.Id ?? '') ||
|
||||
!/^sha256:[0-9a-f]{64}$/u.test(operator?.Id ?? '') ||
|
||||
application?.Architecture !== operator?.Architecture ||
|
||||
!['amd64', 'arm64'].includes(application?.Architecture) ||
|
||||
application?.Config?.User !== '65532:65532' ||
|
||||
operator?.Config?.User !== '65532:65532' ||
|
||||
applicationLabels?.['org.opencontainers.image.title'] !== expectedTitle ||
|
||||
applicationLabels?.['io.qinglong.profile'] !== expectedProfile ||
|
||||
applicationLabels?.['io.qinglong.ai'] !== 'excluded' ||
|
||||
(variant === 'console'
|
||||
? applicationLabels?.['io.qinglong.local.console'] !==
|
||||
'offline-loopback'
|
||||
: applicationLabels?.['io.qinglong.local.console'] !== undefined) ||
|
||||
operatorLabels?.['io.qinglong.lifecycle'] !== 'short-lived' ||
|
||||
operatorLabels?.['io.qinglong.authority'] !== 'local-owner-management' ||
|
||||
!/^[0-9a-f]{40}$/u.test(
|
||||
applicationLabels?.['org.opencontainers.image.revision'] ?? '',
|
||||
) ||
|
||||
applicationLabels?.['org.opencontainers.image.revision'] !==
|
||||
operatorLabels?.['org.opencontainers.image.revision'] ||
|
||||
applicationLabels?.['org.opencontainers.image.version'] !==
|
||||
operatorLabels?.['org.opencontainers.image.version'] ||
|
||||
!/^3\.0\.0-alpha\.[0-9]+$/u.test(
|
||||
applicationLabels?.['org.opencontainers.image.version'] ?? '',
|
||||
)
|
||||
) {
|
||||
fail('image identity, architecture or authority labels drifted');
|
||||
}
|
||||
return Object.freeze({
|
||||
architecture: application[1],
|
||||
applicationId: application[0],
|
||||
operatorId: operator[0],
|
||||
architecture: application.Architecture,
|
||||
applicationId: application.Id,
|
||||
operatorId: operator.Id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -326,6 +342,55 @@ function writeApplicationConfig(state) {
|
||||
},
|
||||
ai: { deployment: 'excluded' },
|
||||
});
|
||||
if (state.variant === 'console') {
|
||||
writePrivateJson(path.join(state.root, 'local-api.json'), {
|
||||
schema: 'qinglong/local-api-process@v1',
|
||||
deploymentRoot: '/var/lib/qinglong3',
|
||||
applicationConfigFilePath:
|
||||
'/var/lib/qinglong3/local-application.json',
|
||||
ownerPepperKeyringDirectory: '/var/lib/qinglong3/owner-peppers',
|
||||
listener: { host: '127.0.0.1', port: 5700 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function delay(milliseconds) {
|
||||
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
async function consoleSurfaceContract() {
|
||||
let lastError;
|
||||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||
try {
|
||||
const root = await fetch('http://127.0.0.1:5700/', {
|
||||
redirect: 'manual',
|
||||
signal: AbortSignal.timeout(2_000),
|
||||
});
|
||||
const api = await fetch(
|
||||
'http://127.0.0.1:5700/api/v3/projects/default/tasks',
|
||||
{
|
||||
redirect: 'manual',
|
||||
signal: AbortSignal.timeout(2_000),
|
||||
},
|
||||
);
|
||||
await root.body?.cancel();
|
||||
await api.body?.cancel();
|
||||
if (root.status !== 200 || api.status !== 401) {
|
||||
fail(
|
||||
`Console HTTP contract drifted: root=${root.status}, unauthenticatedApi=${api.status}`,
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
listener: '127.0.0.1:5700',
|
||||
rootStatus: 200,
|
||||
unauthenticatedApiStatus: 401,
|
||||
});
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
await delay(250);
|
||||
}
|
||||
}
|
||||
throw lastError || new Error('Console listener did not become ready');
|
||||
}
|
||||
|
||||
async function runApplication(state) {
|
||||
@@ -344,7 +409,7 @@ async function runApplication(state) {
|
||||
'--user',
|
||||
`${state.uid}:${state.gid}`,
|
||||
'--network',
|
||||
'none',
|
||||
state.variant === 'console' ? 'host' : 'none',
|
||||
'--cap-drop',
|
||||
'ALL',
|
||||
'--security-opt',
|
||||
@@ -363,13 +428,18 @@ async function runApplication(state) {
|
||||
`${state.root}:/var/lib/qinglong3`,
|
||||
state.applicationImage,
|
||||
'--config',
|
||||
'/var/lib/qinglong3/local-application.json',
|
||||
state.variant === 'console'
|
||||
? '/var/lib/qinglong3/local-api.json'
|
||||
: '/var/lib/qinglong3/local-application.json',
|
||||
],
|
||||
{ stdio: ['ignore', 'pipe', 'pipe'] },
|
||||
);
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let active = false;
|
||||
let surfaceError;
|
||||
let surface = Object.freeze({ status: 'not_applicable' });
|
||||
let surfacePromise = Promise.resolve();
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stderr.setEncoding('utf8');
|
||||
child.stdout.on('data', (chunk) => {
|
||||
@@ -385,7 +455,17 @@ async function runApplication(state) {
|
||||
})
|
||||
) {
|
||||
active = true;
|
||||
docker(['stop', '--time', '30', name]);
|
||||
surfacePromise = (async () => {
|
||||
try {
|
||||
if (state.variant === 'console') {
|
||||
surface = await consoleSurfaceContract();
|
||||
}
|
||||
} catch (error) {
|
||||
surfaceError = error;
|
||||
} finally {
|
||||
docker(['stop', '--time', '30', name]);
|
||||
}
|
||||
})();
|
||||
}
|
||||
});
|
||||
child.stderr.on('data', (chunk) => {
|
||||
@@ -403,6 +483,8 @@ async function runApplication(state) {
|
||||
resolve({ code, signal });
|
||||
});
|
||||
});
|
||||
await surfacePromise;
|
||||
if (surfaceError) throw surfaceError;
|
||||
const events = stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
@@ -426,7 +508,7 @@ async function runApplication(state) {
|
||||
})}`,
|
||||
);
|
||||
}
|
||||
return Object.freeze({ active: true, gracefulStop: true });
|
||||
return Object.freeze({ active: true, gracefulStop: true, surface });
|
||||
} finally {
|
||||
spawnSync('docker', ['rm', '--force', name], { stdio: 'ignore' });
|
||||
}
|
||||
@@ -441,7 +523,11 @@ async function main() {
|
||||
fail('a POSIX identity is required');
|
||||
}
|
||||
const options = argumentsFrom(process.argv.slice(2));
|
||||
const images = inspectImages(options.applicationImage, options.operatorImage);
|
||||
const images = inspectImages(
|
||||
options.applicationImage,
|
||||
options.operatorImage,
|
||||
options.variant,
|
||||
);
|
||||
const root = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-alpha-trial-')),
|
||||
);
|
||||
@@ -479,8 +565,9 @@ async function main() {
|
||||
fail('durable SQLite result is invalid');
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
schema: 'qinglong/local-alpha-trial-kit-live@v1',
|
||||
schemaVersion: 2,
|
||||
schema: 'qinglong/local-alpha-trial-kit-live@v2',
|
||||
variant: options.variant,
|
||||
profile: options.profile,
|
||||
architecture: images.architecture,
|
||||
images: {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const MAX_FILES = 640;
|
||||
const MAX_BYTES = 6 * 1024 * 1024;
|
||||
const EXPECTED_PACKAGES = Object.freeze([
|
||||
'@qinglong/local-admin',
|
||||
'@qinglong/local-api',
|
||||
'@qinglong/local-application',
|
||||
'@qinglong/local-command-file',
|
||||
'@qinglong/local-execution',
|
||||
'@qinglong/local-owner-console',
|
||||
'@qinglong/local-process',
|
||||
'@qinglong/local-secret',
|
||||
'@qinglong/local-sqlite',
|
||||
'@qinglong/runtime-core',
|
||||
'croner',
|
||||
'semver',
|
||||
]);
|
||||
|
||||
function fail(message) {
|
||||
throw new Error(`QingLong local Console image inventory failed: ${message}`);
|
||||
}
|
||||
|
||||
function inventoryRoot(argv) {
|
||||
if (argv.length !== 1 || !argv[0].startsWith('--inventory-root=')) {
|
||||
fail('usage: --inventory-root=/absolute/node_modules');
|
||||
}
|
||||
const root = argv[0].slice('--inventory-root='.length);
|
||||
if (
|
||||
!path.isAbsolute(root) ||
|
||||
path.normalize(root) !== root ||
|
||||
root === path.parse(root).root
|
||||
) {
|
||||
fail('inventory root is invalid');
|
||||
}
|
||||
const stat = fs.lstatSync(root);
|
||||
if (
|
||||
!stat.isDirectory() ||
|
||||
stat.isSymbolicLink() ||
|
||||
fs.realpathSync(root) !== root
|
||||
) {
|
||||
fail('inventory root must be a canonical directory');
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
function packageNames(root) {
|
||||
const result = [];
|
||||
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
||||
if (entry.name.startsWith('.')) continue;
|
||||
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
||||
fail(`unexpected root entry ${entry.name}`);
|
||||
}
|
||||
if (!entry.name.startsWith('@')) {
|
||||
result.push(entry.name);
|
||||
continue;
|
||||
}
|
||||
const scope = path.join(root, entry.name);
|
||||
for (const child of fs.readdirSync(scope, { withFileTypes: true })) {
|
||||
if (
|
||||
child.name.startsWith('.') ||
|
||||
!child.isDirectory() ||
|
||||
child.isSymbolicLink()
|
||||
) {
|
||||
fail(`unexpected scoped entry ${entry.name}/${child.name}`);
|
||||
}
|
||||
result.push(`${entry.name}/${child.name}`);
|
||||
}
|
||||
}
|
||||
return result.sort();
|
||||
}
|
||||
|
||||
function usage(root) {
|
||||
const pending = [root];
|
||||
let files = 0;
|
||||
let bytes = 0;
|
||||
while (pending.length > 0) {
|
||||
const directory = pending.pop();
|
||||
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||
const filePath = path.join(directory, entry.name);
|
||||
const stat = fs.lstatSync(filePath);
|
||||
if (stat.isSymbolicLink()) fail('runtime inventory contains a symlink');
|
||||
if (stat.isDirectory()) {
|
||||
pending.push(filePath);
|
||||
continue;
|
||||
}
|
||||
if (!stat.isFile()) fail('runtime inventory contains a special file');
|
||||
files += 1;
|
||||
bytes += stat.size;
|
||||
if (files > MAX_FILES) fail('runtime file budget exceeded');
|
||||
if (bytes > MAX_BYTES) fail('runtime byte budget exceeded');
|
||||
}
|
||||
}
|
||||
return Object.freeze({ files, bytes });
|
||||
}
|
||||
|
||||
function main() {
|
||||
const root = inventoryRoot(process.argv.slice(2));
|
||||
const packages = packageNames(root);
|
||||
if (JSON.stringify(packages) !== JSON.stringify(EXPECTED_PACKAGES)) {
|
||||
fail(`package closure drifted: ${packages.join(',')}`);
|
||||
}
|
||||
for (const packageName of packages) {
|
||||
const manifest = JSON.parse(
|
||||
fs.readFileSync(path.join(root, packageName, 'package.json'), 'utf8'),
|
||||
);
|
||||
if (
|
||||
manifest.name !== packageName ||
|
||||
typeof manifest.version !== 'string'
|
||||
) {
|
||||
fail(`package identity drifted: ${packageName}`);
|
||||
}
|
||||
}
|
||||
const measured = usage(root);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
image: 'local-console',
|
||||
packages,
|
||||
packageCount: packages.length,
|
||||
files: measured.files,
|
||||
bytes: measured.bytes,
|
||||
maxFiles: MAX_FILES,
|
||||
maxBytes: MAX_BYTES,
|
||||
ai: 'excluded',
|
||||
listener: 'loopback-only',
|
||||
compatible: true,
|
||||
})}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
process.stderr.write(
|
||||
`${error instanceof Error ? error.message : String(error)}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -29,6 +29,18 @@ const RUNTIME_DEPENDENCIES = Object.freeze({
|
||||
});
|
||||
const BUILD_PACKAGES = Object.freeze([
|
||||
'ql3-ai',
|
||||
'ql3-local-admin',
|
||||
'ql3-local-api',
|
||||
'ql3-local-application',
|
||||
'ql3-local-command-file',
|
||||
'ql3-local-execution',
|
||||
'ql3-local-owner-console',
|
||||
'ql3-local-process',
|
||||
'ql3-local-secret',
|
||||
'ql3-local-sqlite',
|
||||
'ql3-runtime-core',
|
||||
]);
|
||||
const RUNTIME_PACKAGES = Object.freeze([
|
||||
'ql3-local-admin',
|
||||
'ql3-local-application',
|
||||
'ql3-local-command-file',
|
||||
@@ -38,8 +50,8 @@ const BUILD_PACKAGES = Object.freeze([
|
||||
'ql3-local-sqlite',
|
||||
'ql3-runtime-core',
|
||||
]);
|
||||
const RUNTIME_PACKAGES = Object.freeze(
|
||||
BUILD_PACKAGES.filter((name) => name !== 'ql3-ai'),
|
||||
const CONSOLE_RUNTIME_PACKAGES = Object.freeze(
|
||||
[...RUNTIME_PACKAGES, 'ql3-local-api', 'ql3-local-owner-console'].sort(),
|
||||
);
|
||||
|
||||
function readJson(filePath) {
|
||||
@@ -174,6 +186,15 @@ function counts(values) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function dockerStage(contents, name) {
|
||||
const marker = new RegExp(`^FROM [^\\n]+ AS ${name}$`, 'm');
|
||||
const match = marker.exec(contents);
|
||||
if (!match) return '';
|
||||
const remaining = contents.slice(match.index + match[0].length);
|
||||
const next = /^FROM [^\n]+ AS [a-z0-9-]+$/m.exec(remaining);
|
||||
return next ? remaining.slice(0, next.index) : remaining;
|
||||
}
|
||||
|
||||
function auditDockerfile(contents, findings) {
|
||||
const escapedBuildNodeImage = BUILD_NODE_IMAGE.replace(
|
||||
/[.*+?^${}()|[\]\\]/g,
|
||||
@@ -188,7 +209,7 @@ function auditDockerfile(contents, findings) {
|
||||
'gm',
|
||||
);
|
||||
const exactRuntimeBasePattern = new RegExp(
|
||||
`^FROM ${escapedRuntimeNodeImage} AS runtime$`,
|
||||
`^FROM ${escapedRuntimeNodeImage} AS runtime-platform$`,
|
||||
'gm',
|
||||
);
|
||||
if (
|
||||
@@ -221,8 +242,13 @@ function auditDockerfile(contents, findings) {
|
||||
addFinding(findings, 'UNREVIEWED_RUNTIME_SURFACE');
|
||||
}
|
||||
|
||||
const workspaceStage = dockerStage(contents, 'workspace');
|
||||
const assembledStage = dockerStage(contents, 'assembled');
|
||||
const consoleAssembledStage = dockerStage(contents, 'console-assembled');
|
||||
const runtimeStage = dockerStage(contents, 'runtime');
|
||||
const consoleRuntimeStage = dockerStage(contents, 'runtime-console');
|
||||
const buildCopies = captures(
|
||||
contents,
|
||||
workspaceStage,
|
||||
/^COPY packages\/(ql3-[a-z-]+) packages\/\1$/gm,
|
||||
).sort();
|
||||
if (!sameJson(buildCopies, [...BUILD_PACKAGES].sort())) {
|
||||
@@ -230,7 +256,7 @@ function auditDockerfile(contents, findings) {
|
||||
}
|
||||
const runtimeCopyCounts = counts(
|
||||
captures(
|
||||
contents,
|
||||
assembledStage,
|
||||
/^COPY --from=workspace \/workspace\/packages\/(ql3-[a-z-]+)\/(?:package\.json|dist) /gm,
|
||||
),
|
||||
);
|
||||
@@ -244,6 +270,28 @@ function auditDockerfile(contents, findings) {
|
||||
) {
|
||||
addFinding(findings, 'RUNTIME_INTERNAL_PACKAGE_CLOSURE_DRIFT');
|
||||
}
|
||||
const consoleRuntimeCopyCounts = counts(
|
||||
captures(
|
||||
consoleAssembledStage,
|
||||
/^COPY --from=workspace \/workspace\/packages\/(ql3-[a-z-]+)\/(?:package\.json|dist) /gm,
|
||||
),
|
||||
);
|
||||
if (
|
||||
!sameJson(
|
||||
sortedObject(consoleRuntimeCopyCounts),
|
||||
sortedObject(
|
||||
Object.fromEntries(
|
||||
CONSOLE_RUNTIME_PACKAGES.map((name) => [name, 2]),
|
||||
),
|
||||
),
|
||||
) ||
|
||||
!consoleAssembledStage.includes(
|
||||
'COPY --from=workspace /workspace/packages/ql3-local-api/assets \\\n' +
|
||||
' node_modules/@qinglong/local-api/assets',
|
||||
)
|
||||
) {
|
||||
addFinding(findings, 'CONSOLE_RUNTIME_INTERNAL_PACKAGE_CLOSURE_DRIFT');
|
||||
}
|
||||
if (contents.includes('COPY --from=workspace /workspace/packages/ql3-ai/')) {
|
||||
addFinding(findings, 'AI_PRESENT_IN_RUNTIME_STAGE');
|
||||
}
|
||||
@@ -264,20 +312,51 @@ function auditDockerfile(contents, findings) {
|
||||
addFinding(findings, 'RUNTIME_NONESSENTIAL_FILES_NOT_REMOVED');
|
||||
}
|
||||
if (
|
||||
!contents.includes('USER 65532:65532') ||
|
||||
!contents.includes(
|
||||
!consoleAssembledStage.includes(
|
||||
'RUN rm -rf node_modules/.bin \\\n' +
|
||||
' && node /tmp/ql3-prune-runtime-artifact.cjs node_modules/@qinglong \\\n' +
|
||||
' @qinglong/local-api/config \\\n' +
|
||||
' @qinglong/local-api/process \\\n' +
|
||||
' --exclude=@qinglong/ai \\\n' +
|
||||
' --retain-js=local-api/assets/console/console.js \\\n' +
|
||||
' && rm /tmp/ql3-prune-runtime-artifact.cjs',
|
||||
)
|
||||
) {
|
||||
addFinding(findings, 'CONSOLE_RUNTIME_NONESSENTIAL_FILES_NOT_REMOVED');
|
||||
}
|
||||
if (
|
||||
!runtimeStage.includes('USER 65532:65532') ||
|
||||
!runtimeStage.includes(
|
||||
'ENTRYPOINT ["node", "/opt/qinglong/node_modules/@qinglong/local-application/dist/cli.js"]',
|
||||
) ||
|
||||
!contents.includes('io.qinglong.ai="excluded"') ||
|
||||
!contents.includes('io.qinglong.profile="edge,standalone"') ||
|
||||
!contents.includes('io.qinglong.local.application-config="2,3,4"') ||
|
||||
!contents.includes('io.qinglong.local.sqlite-contract-min="51"') ||
|
||||
!contents.includes('io.qinglong.local.sqlite-contract-max="52"') ||
|
||||
!contents.includes('io.qinglong.local.sqlite-write-contract="52"') ||
|
||||
!contents.includes('io.qinglong.local.compose-selection="1"')
|
||||
!runtimeStage.includes('io.qinglong.ai="excluded"') ||
|
||||
!runtimeStage.includes('io.qinglong.profile="edge,standalone"') ||
|
||||
!runtimeStage.includes('io.qinglong.local.application-config="2,3,4"') ||
|
||||
!runtimeStage.includes('io.qinglong.local.sqlite-contract-min="51"') ||
|
||||
!runtimeStage.includes('io.qinglong.local.sqlite-contract-max="52"') ||
|
||||
!runtimeStage.includes('io.qinglong.local.sqlite-write-contract="52"') ||
|
||||
!runtimeStage.includes('io.qinglong.local.compose-selection="1"')
|
||||
) {
|
||||
addFinding(findings, 'RUNTIME_IDENTITY_OR_LABEL_DRIFT');
|
||||
}
|
||||
if (
|
||||
!consoleRuntimeStage.includes('USER 65532:65532') ||
|
||||
!consoleRuntimeStage.includes(
|
||||
'ENTRYPOINT ["node", "/opt/qinglong/node_modules/@qinglong/local-api/dist/cli.js"]',
|
||||
) ||
|
||||
!consoleRuntimeStage.includes(
|
||||
'org.opencontainers.image.title="QingLong 3.0 Local Console Application"',
|
||||
) ||
|
||||
!consoleRuntimeStage.includes(
|
||||
'io.qinglong.profile="edge-application-api,standalone-application-api"',
|
||||
) ||
|
||||
!consoleRuntimeStage.includes(
|
||||
'io.qinglong.local.console="offline-loopback"',
|
||||
) ||
|
||||
!consoleRuntimeStage.includes('io.qinglong.ai="excluded"')
|
||||
) {
|
||||
addFinding(findings, 'CONSOLE_RUNTIME_IDENTITY_OR_LABEL_DRIFT');
|
||||
}
|
||||
}
|
||||
|
||||
function auditWorkflow(contents, findings) {
|
||||
@@ -295,6 +374,9 @@ function auditWorkflow(contents, findings) {
|
||||
'pnpm audit:local-image:ql3',
|
||||
'docker build',
|
||||
'--file deploy/containers/ql3-local-application/Dockerfile',
|
||||
'--target runtime',
|
||||
'--target runtime-console',
|
||||
'qinglong3-local-console:ci-${{ matrix.image_arch }}',
|
||||
'EXPECTED: ${{ matrix.image_arch }} 65532:65532 2,3,4 51 52 52 1',
|
||||
'actual="$(docker image inspect --format \'{{.Architecture}} {{.Config.User}} {{index .Config.Labels "io.qinglong.local.application-config"}} {{index .Config.Labels "io.qinglong.local.sqlite-contract-min"}} {{index .Config.Labels "io.qinglong.local.sqlite-contract-max"}} {{index .Config.Labels "io.qinglong.local.sqlite-write-contract"}} {{index .Config.Labels "io.qinglong.local.compose-selection"}}\' "${IMAGE}")"',
|
||||
'io.qinglong.local.application-config',
|
||||
@@ -309,10 +391,18 @@ function auditWorkflow(contents, findings) {
|
||||
'--memory=128m',
|
||||
'--pids-limit=64',
|
||||
'scripts/ql3-local-image-inventory.cjs',
|
||||
'scripts/ql3-local-console-image-inventory.cjs',
|
||||
'--inventory-root=/opt/qinglong/node_modules',
|
||||
'node ../../scripts/ql3-build-package-closure.cjs',
|
||||
'node scripts/ql3-local-image-live-contract.cjs --image="${IMAGE}" --profile=edge',
|
||||
'node scripts/ql3-local-image-live-contract.cjs --image="${IMAGE}" --profile=standalone',
|
||||
'node scripts/ql3-local-alpha-trial-kit-live-contract.cjs',
|
||||
'--variant=headless',
|
||||
'--variant=console',
|
||||
'--image=local-console',
|
||||
'io.qinglong.local.console',
|
||||
'--variant="${TRIAL_VARIANT}"',
|
||||
'inputs.local_alpha_variant',
|
||||
];
|
||||
for (const value of required) {
|
||||
if (!job.includes(value)) {
|
||||
@@ -356,6 +446,14 @@ function auditLocalImageContract(root) {
|
||||
...Object.keys(RUNTIME_DEPENDENCIES),
|
||||
].sort(),
|
||||
),
|
||||
consoleRuntimePackages: Object.freeze(
|
||||
[
|
||||
...CONSOLE_RUNTIME_PACKAGES.map(
|
||||
(name) => `@qinglong/${name.slice(4)}`,
|
||||
),
|
||||
...Object.keys(RUNTIME_DEPENDENCIES),
|
||||
].sort(),
|
||||
),
|
||||
findings: Object.freeze(findings),
|
||||
compatible: findings.length === 0,
|
||||
});
|
||||
|
||||
@@ -199,10 +199,10 @@ function auditWorkflow(contents, findings) {
|
||||
'--mode=record-verification',
|
||||
'--mode=create',
|
||||
'--mode=audit',
|
||||
'/quickstart.sh" \\\n edge "${QUICKSTART_ROOT}" "${QUICKSTART_CONTAINER}"',
|
||||
'sh "${BUNDLE_ROOT}/quickstart.sh" \\\n edge "${QUICKSTART_ROOT}" "${QUICKSTART_CONTAINER}"',
|
||||
'docker stop --time 30 "${QUICKSTART_CONTAINER}"',
|
||||
'test -s "${QUICKSTART_ROOT}/qinglong3.sqlite"',
|
||||
'--application-sbom="${RUNNER_TEMP}/ql3-local-application.cdx.json"',
|
||||
'--application-sbom="${APPLICATION_SBOM}"',
|
||||
'--operator-sbom="${RUNNER_TEMP}/ql3-local-operator.cdx.json"',
|
||||
'--verification-evidence="${RUNNER_TEMP}/ql3-local-alpha-verification-${{ matrix.image_arch }}.json"',
|
||||
'--readme=docs/operations/ql3-local-alpha-trial-kit.md',
|
||||
@@ -213,6 +213,8 @@ function auditWorkflow(contents, findings) {
|
||||
'--job=${{ github.job }}',
|
||||
'--run-id=${{ github.run_id }}',
|
||||
'--run-attempt=${{ github.run_attempt }}',
|
||||
'--variant="${TRIAL_VARIANT}"',
|
||||
'inputs.local_alpha_variant',
|
||||
];
|
||||
for (const value of required) {
|
||||
if (!contents.includes(value))
|
||||
|
||||
@@ -760,17 +760,22 @@ if (require.main === module) {
|
||||
try {
|
||||
if (process.argv.length < 4) {
|
||||
fail(
|
||||
'usage: ql3-prune-runtime-artifact.cjs node_modules/@qinglong @qinglong/profile-entry [...]',
|
||||
'usage: ql3-prune-runtime-artifact.cjs node_modules/@qinglong @qinglong/profile-entry [...] [--exclude=@qinglong/package] [--retain-js=package/path.js]',
|
||||
);
|
||||
}
|
||||
const arguments = process.argv.slice(3);
|
||||
const report = pruneRuntimeArtifact(process.argv[2], {
|
||||
entrySpecifiers: arguments.filter(
|
||||
(argument) => !argument.startsWith('--exclude='),
|
||||
(argument) =>
|
||||
!argument.startsWith('--exclude=') &&
|
||||
!argument.startsWith('--retain-js='),
|
||||
),
|
||||
excludedInternalPackages: arguments
|
||||
.filter((argument) => argument.startsWith('--exclude='))
|
||||
.map((argument) => argument.slice('--exclude='.length)),
|
||||
retainedJavaScriptFiles: arguments
|
||||
.filter((argument) => argument.startsWith('--retain-js='))
|
||||
.map((argument) => argument.slice('--retain-js='.length)),
|
||||
});
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ schemaVersion: 1, ...report })}\n`,
|
||||
|
||||
@@ -25,6 +25,12 @@ const CONTAINER_ROOTS = Object.freeze([
|
||||
'deploy/containers/ql3-local-application',
|
||||
'deploy/containers/ql3-worker',
|
||||
]);
|
||||
const CONTAINER_DOCKER_VERSION_LABEL_COUNTS = Object.freeze({
|
||||
'deploy/containers/ql3-cluster-control': 1,
|
||||
'deploy/containers/ql3-cluster-admin': 1,
|
||||
'deploy/containers/ql3-local-application': 2,
|
||||
'deploy/containers/ql3-worker': 1,
|
||||
});
|
||||
const DEPLOYMENT_ROOTS = Object.freeze([
|
||||
'deploy/kubernetes/ql3-cluster',
|
||||
'deploy/kubernetes/ql3-worker',
|
||||
@@ -257,7 +263,7 @@ function auditReleaseVersionContract(rootInput = DEFAULT_ROOT) {
|
||||
versionOccurrences(
|
||||
dockerfile,
|
||||
`org.opencontainers.image.version=\"${identity.version}\"`,
|
||||
) !== 1
|
||||
) !== CONTAINER_DOCKER_VERSION_LABEL_COUNTS[containerRoot]
|
||||
) {
|
||||
fail(`container Dockerfile release identity drifted: ${containerRoot}`);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ OPERATOR_ID='@@OPERATOR_ID@@'
|
||||
ARCHITECTURE='@@ARCHITECTURE@@'
|
||||
SOURCE_REVISION='@@SOURCE_REVISION@@'
|
||||
ARCHIVE='@@ARCHIVE@@'
|
||||
VARIANT='@@VARIANT@@'
|
||||
|
||||
fail() {
|
||||
printf '%s\n' "QingLong Local Alpha quickstart failed: $*" >&2
|
||||
@@ -37,6 +38,18 @@ case "$profile" in
|
||||
;;
|
||||
*) usage ;;
|
||||
esac
|
||||
case "$VARIANT" in
|
||||
headless)
|
||||
network_mode=none
|
||||
application_config=local-application.json
|
||||
;;
|
||||
console)
|
||||
[ "$(uname -s)" = Linux ] || fail 'Console variant requires a Linux Docker host'
|
||||
network_mode=host
|
||||
application_config=local-api.json
|
||||
;;
|
||||
*) fail 'embedded Trial Kit variant is invalid' ;;
|
||||
esac
|
||||
|
||||
case "$data_root" in
|
||||
/|*[!A-Za-z0-9_./-]*|*'/../'*|*'/./'*|*'/..'|*'/.'|*'//'*|*/)
|
||||
@@ -92,6 +105,11 @@ EOF
|
||||
cat >"$data_root/local-application.json" <<EOF
|
||||
{"schema":"qinglong/local-application-process@v2","instanceId":"alpha-trial-local","profile":"$profile","storage":{"mode":"fresh","databasePath":"/var/lib/qinglong3/qinglong3.sqlite","busyTimeoutMs":100},"runtime":{"receiptRoot":"/var/lib/qinglong3/receipts","artifactRoot":"/var/lib/qinglong3/artifacts","secretKeyringPath":"/var/lib/qinglong3/local-secret-keyring.json"},"pluginPackages":{"stagingRoot":"/var/lib/qinglong3/plugin-staging","activationRoot":"/var/lib/qinglong3/plugin-activation","recoverySource":{"mode":"disabled"},"pageSize":4,"maxPages":4,"taskPublicationPageSize":4,"taskPublicationMaxPages":4},"ai":{"deployment":"excluded"}}
|
||||
EOF
|
||||
if [ "$VARIANT" = console ]; then
|
||||
cat >"$data_root/local-api.json" <<EOF
|
||||
{"schema":"qinglong/local-api-process@v1","deploymentRoot":"/var/lib/qinglong3","applicationConfigFilePath":"/var/lib/qinglong3/local-application.json","ownerPepperKeyringDirectory":"/var/lib/qinglong3/owner-peppers","listener":{"host":"127.0.0.1","port":5700}}
|
||||
EOF
|
||||
fi
|
||||
chmod 0600 "$data_root"/*.json
|
||||
|
||||
uid=$(id -u)
|
||||
@@ -129,12 +147,12 @@ trap cleanup EXIT
|
||||
trap 'exit 130' HUP INT TERM
|
||||
|
||||
container_id=$(docker run --detach --name "$container_name" \
|
||||
--restart unless-stopped --read-only --user "$uid:$gid" --network none \
|
||||
--restart unless-stopped --read-only --user "$uid:$gid" --network "$network_mode" \
|
||||
--cap-drop ALL --security-opt no-new-privileges \
|
||||
--memory "$memory" --memory-swap "$memory" --cpus 0.5 --pids-limit "$pids" \
|
||||
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=16m \
|
||||
--mount "type=bind,src=$data_root,dst=/var/lib/qinglong3" \
|
||||
"$APPLICATION_IMAGE" --config /var/lib/qinglong3/local-application.json)
|
||||
"$APPLICATION_IMAGE" --config "/var/lib/qinglong3/$application_config")
|
||||
printf '%s\n' "$container_id" >"$data_root/container.id"
|
||||
chmod 0600 "$data_root/container.id"
|
||||
|
||||
@@ -153,10 +171,15 @@ done
|
||||
umask "$old_umask"
|
||||
|
||||
printf '%s\n' \
|
||||
"QingLong 3.0 Local Alpha is active ($profile, $ARCHITECTURE)." \
|
||||
"QingLong 3.0 Local Alpha is active ($VARIANT, $profile, $ARCHITECTURE)." \
|
||||
"Data root: $data_root" \
|
||||
"Owner deliveries: $data_root/owner-delivery" \
|
||||
"Logs: docker logs $container_name" \
|
||||
"Stop: docker stop --time 30 $container_name" \
|
||||
"Remove container: docker rm $container_name" \
|
||||
'The fresh data root is retained until you remove it explicitly.'
|
||||
if [ "$VARIANT" = console ]; then
|
||||
printf '%s\n' \
|
||||
'Console: http://127.0.0.1:5700/' \
|
||||
'Remote access: create an SSH tunnel to 127.0.0.1:5700; do not expose the port on LAN or the public Internet.'
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user