mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
feat(ql3): ship runnable local alpha quickstart
This commit is contained in:
@@ -10,18 +10,24 @@ 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@v2';
|
||||
const SCHEMA = 'qinglong/alpha-local-trial-kit@v3';
|
||||
const VERIFICATION_SCHEMA = 'qinglong/alpha-local-trial-kit-verification@v1';
|
||||
const QUICKSTART_TEMPLATE = path.join(
|
||||
DEFAULT_ROOT,
|
||||
'scripts/templates/ql3-local-alpha-quickstart.sh',
|
||||
);
|
||||
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
||||
const ARCHIVE_MIN_BYTES = 1024;
|
||||
const MAX_JSON_BYTES = 4 * 1024 * 1024;
|
||||
const MAX_README_BYTES = 512 * 1024;
|
||||
const MAX_QUICKSTART_BYTES = 256 * 1024;
|
||||
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
||||
const REVISION_PATTERN = /^[0-9a-f]{40}$/u;
|
||||
const FILES = Object.freeze({
|
||||
applicationSbom: 'qinglong3-local-application.cdx.json',
|
||||
operatorSbom: 'qinglong3-local-operator.cdx.json',
|
||||
verificationEvidence: 'verification-evidence.json',
|
||||
quickstart: 'quickstart.sh',
|
||||
readme: 'README.md',
|
||||
manifest: 'manifest.json',
|
||||
checksums: 'SHA256SUMS',
|
||||
@@ -148,9 +154,7 @@ function saveDockerImages(images, archivePath) {
|
||||
function validateImageReference(value, label) {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
value.length < 3 ||
|
||||
value.length > 256 ||
|
||||
/[\s\0]/u.test(value)
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._:/@-]{2,255}$/u.test(value)
|
||||
) {
|
||||
fail(`${label} image reference is invalid`);
|
||||
}
|
||||
@@ -383,6 +387,34 @@ function archiveName(architecture) {
|
||||
return `qinglong3-local-trial-kit-${architecture}.docker.tar`;
|
||||
}
|
||||
|
||||
function renderQuickstart(identity) {
|
||||
const template = fs.readFileSync(
|
||||
assertCanonicalFile(
|
||||
QUICKSTART_TEMPLATE,
|
||||
MAX_QUICKSTART_BYTES,
|
||||
'quickstart template',
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
const replacements = Object.freeze({
|
||||
'@@APPLICATION_IMAGE@@': identity.images.application.reference,
|
||||
'@@APPLICATION_ID@@': identity.images.application.id,
|
||||
'@@OPERATOR_IMAGE@@': identity.images.operator.reference,
|
||||
'@@OPERATOR_ID@@': identity.images.operator.id,
|
||||
'@@ARCHITECTURE@@': identity.architecture,
|
||||
'@@SOURCE_REVISION@@': identity.sourceRevision,
|
||||
'@@ARCHIVE@@': identity.archive.file,
|
||||
});
|
||||
let rendered = template;
|
||||
for (const [token, value] of Object.entries(replacements)) {
|
||||
rendered = rendered.replaceAll(token, value);
|
||||
}
|
||||
if (/@@[A-Z_]+@@/u.test(rendered)) {
|
||||
fail('quickstart template contains an unresolved token');
|
||||
}
|
||||
return rendered;
|
||||
}
|
||||
|
||||
function fileRecord(bundleRoot, name) {
|
||||
const filePath = path.join(bundleRoot, name);
|
||||
const stat = fs.lstatSync(filePath);
|
||||
@@ -541,8 +573,19 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
normalized.readme,
|
||||
path.join(normalized.outputRoot, FILES.readme),
|
||||
);
|
||||
const manifestIdentity = {
|
||||
sourceRevision: normalized.sourceRevision,
|
||||
architecture: normalized.architecture,
|
||||
archive: { file: archive },
|
||||
images: { application, operator },
|
||||
};
|
||||
writeExclusive(
|
||||
path.join(normalized.outputRoot, FILES.quickstart),
|
||||
renderQuickstart(manifestIdentity),
|
||||
0o700,
|
||||
);
|
||||
const manifest = {
|
||||
schemaVersion: 3,
|
||||
schemaVersion: 4,
|
||||
schema: SCHEMA,
|
||||
maturity: 'alpha_candidate_not_public_release',
|
||||
product: 'local',
|
||||
@@ -555,6 +598,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
application: fileRecord(normalized.outputRoot, FILES.applicationSbom),
|
||||
operator: fileRecord(normalized.outputRoot, FILES.operatorSbom),
|
||||
},
|
||||
quickstart: fileRecord(normalized.outputRoot, FILES.quickstart),
|
||||
readme: fileRecord(normalized.outputRoot, FILES.readme),
|
||||
verification: fileRecord(
|
||||
normalized.outputRoot,
|
||||
@@ -570,6 +614,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
FILES.applicationSbom,
|
||||
FILES.operatorSbom,
|
||||
FILES.verificationEvidence,
|
||||
FILES.quickstart,
|
||||
FILES.readme,
|
||||
FILES.manifest,
|
||||
];
|
||||
@@ -637,10 +682,11 @@ function auditLocalAlphaTrialKit(options) {
|
||||
'archive',
|
||||
'images',
|
||||
'sboms',
|
||||
'quickstart',
|
||||
'readme',
|
||||
'verification',
|
||||
]) ||
|
||||
manifest.schemaVersion !== 3 ||
|
||||
manifest.schemaVersion !== 4 ||
|
||||
manifest.schema !== SCHEMA ||
|
||||
manifest.maturity !== 'alpha_candidate_not_public_release' ||
|
||||
manifest.product !== 'local' ||
|
||||
@@ -673,6 +719,19 @@ function auditLocalAlphaTrialKit(options) {
|
||||
FILES.verificationEvidence,
|
||||
bundleRoot,
|
||||
);
|
||||
validateFileRecord(manifest.quickstart, FILES.quickstart, bundleRoot);
|
||||
const expectedQuickstart = renderQuickstart(manifest);
|
||||
const actualQuickstart = fs.readFileSync(
|
||||
assertCanonicalFile(
|
||||
path.join(bundleRoot, FILES.quickstart),
|
||||
MAX_QUICKSTART_BYTES,
|
||||
'quickstart',
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
if (actualQuickstart !== expectedQuickstart) {
|
||||
fail('quickstart differs from the canonical deployment journey');
|
||||
}
|
||||
validateFileRecord(manifest.readme, FILES.readme, bundleRoot);
|
||||
validateOfflineSbom(
|
||||
readBoundedJson(
|
||||
@@ -707,6 +766,7 @@ function auditLocalAlphaTrialKit(options) {
|
||||
FILES.applicationSbom,
|
||||
FILES.operatorSbom,
|
||||
FILES.verificationEvidence,
|
||||
FILES.quickstart,
|
||||
expectedArchive,
|
||||
].sort();
|
||||
const actualFiles = fs
|
||||
@@ -726,6 +786,7 @@ function auditLocalAlphaTrialKit(options) {
|
||||
FILES.applicationSbom,
|
||||
FILES.operatorSbom,
|
||||
FILES.verificationEvidence,
|
||||
FILES.quickstart,
|
||||
FILES.readme,
|
||||
FILES.manifest,
|
||||
];
|
||||
@@ -746,6 +807,7 @@ function auditLocalAlphaTrialKit(options) {
|
||||
archiveSha256: manifest.archive.sha256,
|
||||
applicationImageId: manifest.images.application.id,
|
||||
operatorImageId: manifest.images.operator.id,
|
||||
quickstartSha256: manifest.quickstart.sha256,
|
||||
verificationSha256: manifest.verification.sha256,
|
||||
workflowRunId: verificationEvidence.workflow.runId,
|
||||
workflowRunAttempt: verificationEvidence.workflow.runAttempt,
|
||||
|
||||
@@ -199,6 +199,9 @@ function auditWorkflow(contents, findings) {
|
||||
'--mode=record-verification',
|
||||
'--mode=create',
|
||||
'--mode=audit',
|
||||
'/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"',
|
||||
'--operator-sbom="${RUNNER_TEMP}/ql3-local-operator.cdx.json"',
|
||||
'--verification-evidence="${RUNNER_TEMP}/ql3-local-alpha-verification-${{ matrix.image_arch }}.json"',
|
||||
@@ -222,6 +225,7 @@ function auditWorkflow(contents, findings) {
|
||||
'--mode=record-verification',
|
||||
'--mode=create',
|
||||
'--mode=audit',
|
||||
'/quickstart.sh"',
|
||||
'name: Upload the tested native Local Alpha trial kit',
|
||||
]) {
|
||||
const index = contents.indexOf(value, cursor + 1);
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
APPLICATION_IMAGE='@@APPLICATION_IMAGE@@'
|
||||
APPLICATION_ID='@@APPLICATION_ID@@'
|
||||
OPERATOR_IMAGE='@@OPERATOR_IMAGE@@'
|
||||
OPERATOR_ID='@@OPERATOR_ID@@'
|
||||
ARCHITECTURE='@@ARCHITECTURE@@'
|
||||
SOURCE_REVISION='@@SOURCE_REVISION@@'
|
||||
ARCHIVE='@@ARCHIVE@@'
|
||||
|
||||
fail() {
|
||||
printf '%s\n' "QingLong Local Alpha quickstart failed: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
usage() {
|
||||
printf '%s\n' \
|
||||
'usage: sh quickstart.sh edge|standalone /absolute/new/data-root [container-name]' >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
[ "$#" -ge 2 ] && [ "$#" -le 3 ] || usage
|
||||
profile=$1
|
||||
data_root=$2
|
||||
container_name=${3:-ql3-alpha-local}
|
||||
|
||||
case "$profile" in
|
||||
edge)
|
||||
memory=128m
|
||||
pids=64
|
||||
;;
|
||||
standalone)
|
||||
memory=256m
|
||||
pids=256
|
||||
;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
case "$data_root" in
|
||||
/|*[!A-Za-z0-9_./-]*|*'/../'*|*'/./'*|*'/..'|*'/.'|*'//'*|*/)
|
||||
fail 'data root must be a canonical absolute path using A-Z, a-z, 0-9, _, ., / or -'
|
||||
;;
|
||||
/*) ;;
|
||||
*) fail 'data root must be absolute' ;;
|
||||
esac
|
||||
case "$container_name" in
|
||||
''|[_.-]*|*[!A-Za-z0-9_.-]*) fail 'container name is invalid' ;;
|
||||
esac
|
||||
|
||||
command -v docker >/dev/null 2>&1 || fail 'docker is required'
|
||||
command -v sha256sum >/dev/null 2>&1 || fail 'sha256sum is required'
|
||||
command -v grep >/dev/null 2>&1 || fail 'grep is required'
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||
parent=${data_root%/*}
|
||||
[ -n "$parent" ] || parent=/
|
||||
[ -d "$parent" ] || fail 'data root parent does not exist'
|
||||
parent_real=$(CDPATH= cd -- "$parent" && pwd -P)
|
||||
[ "$parent_real/${data_root##*/}" = "$data_root" ] || fail 'data root parent is not canonical'
|
||||
[ ! -e "$data_root" ] || fail 'data root must not already exist'
|
||||
|
||||
(CDPATH= cd -- "$script_dir" && sha256sum --check SHA256SUMS)
|
||||
docker info >/dev/null 2>&1 || fail 'docker daemon is unavailable'
|
||||
docker load --input "$script_dir/$ARCHIVE" >/dev/null
|
||||
|
||||
application_identity=$(docker image inspect --format '{{.Id}}|{{.Architecture}}|{{.Config.User}}|{{index .Config.Labels "org.opencontainers.image.revision"}}' "$APPLICATION_IMAGE")
|
||||
operator_identity=$(docker image inspect --format '{{.Id}}|{{.Architecture}}|{{.Config.User}}|{{index .Config.Labels "org.opencontainers.image.revision"}}|{{index .Config.Labels "io.qinglong.lifecycle"}}|{{index .Config.Labels "io.qinglong.network"}}' "$OPERATOR_IMAGE")
|
||||
[ "$application_identity" = "$APPLICATION_ID|$ARCHITECTURE|65532:65532|$SOURCE_REVISION" ] || fail 'application image identity is incompatible'
|
||||
[ "$operator_identity" = "$OPERATOR_ID|$ARCHITECTURE|65532:65532|$SOURCE_REVISION|short-lived|none-by-default" ] || fail 'operator image identity is incompatible'
|
||||
|
||||
old_umask=$(umask)
|
||||
umask 077
|
||||
mkdir -m 0700 "$data_root"
|
||||
for directory in owner-peppers owner-pepper-backup owner-delivery receipts artifacts plugin-staging plugin-activation results; do
|
||||
mkdir -m 0700 "$data_root/$directory"
|
||||
done
|
||||
|
||||
cat >"$data_root/setup.json" <<EOF
|
||||
{"schemaVersion":1,"operation":"local.setup.prepare","options":{"deploymentRoot":"/var/lib/qinglong3","databasePath":"/var/lib/qinglong3/qinglong3.sqlite","profile":"$profile","ownerPepperKeyringDirectory":"/var/lib/qinglong3/owner-peppers","ownerPepperBackupDirectory":"/var/lib/qinglong3/owner-pepper-backup","ownerPepperKeyId":"owner-v1","localSecretKeyringPath":"/var/lib/qinglong3/local-secret-keyring.json","busyTimeoutMs":100},"request":{"registerMutationId":"019f8680-143d-4000-8000-000000000011","activateMutationId":"019f8680-143d-4000-8000-000000000012","registeredAtMs":1785254400000,"activatedAtMs":1785254400001}}
|
||||
EOF
|
||||
cat >"$data_root/owner-provision.json" <<EOF
|
||||
{"schemaVersion":1,"operation":"owner.identity.provision","options":{"deploymentRoot":"/var/lib/qinglong3","databasePath":"/var/lib/qinglong3/qinglong3.sqlite","pepperPath":"/var/lib/qinglong3/owner-peppers/b3duZXItdjE.pepper","pepperKeyId":"owner-v1","secretDeliveryDirectory":"/var/lib/qinglong3/owner-delivery","profile":"$profile","busyTimeoutMs":100},"request":{"mutationId":"019f8680-143d-4000-8000-000000000021","requestId":"alpha-trial-owner-provision"}}
|
||||
EOF
|
||||
cat >"$data_root/owner-challenge.json" <<EOF
|
||||
{"schemaVersion":1,"operation":"owner.challenge.issue","options":{"deploymentRoot":"/var/lib/qinglong3","databasePath":"/var/lib/qinglong3/qinglong3.sqlite","pepperPath":"/var/lib/qinglong3/owner-peppers/b3duZXItdjE.pepper","pepperKeyId":"owner-v1","secretDeliveryDirectory":"/var/lib/qinglong3/owner-delivery","profile":"$profile","busyTimeoutMs":100},"request":{"projectId":"default","mutationId":"019f8680-143d-4000-8000-000000000022","requestId":"alpha-trial-owner-challenge"}}
|
||||
EOF
|
||||
cat >"$data_root/owner-claim.json" <<EOF
|
||||
{"schemaVersion":1,"operation":"owner.claim.from-deliveries","options":{"deploymentRoot":"/var/lib/qinglong3","databasePath":"/var/lib/qinglong3/qinglong3.sqlite","pepperPath":"/var/lib/qinglong3/owner-peppers/b3duZXItdjE.pepper","pepperKeyId":"owner-v1","secretDeliveryDirectory":"/var/lib/qinglong3/owner-delivery","profile":"$profile","busyTimeoutMs":100},"request":{"projectId":"default","mutationId":"019f8680-143d-4000-8000-000000000023","requestId":"alpha-trial-owner-claim","credentialMutationId":"019f8680-143d-4000-8000-000000000021","challengeMutationId":"019f8680-143d-4000-8000-000000000022"}}
|
||||
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
|
||||
chmod 0600 "$data_root"/*.json
|
||||
|
||||
uid=$(id -u)
|
||||
gid=$(id -g)
|
||||
run_operator() {
|
||||
command_name=$1
|
||||
command_file=$2
|
||||
docker run --rm --read-only --user "$uid:$gid" --network none \
|
||||
--cap-drop ALL --security-opt no-new-privileges \
|
||||
--memory 128m --memory-swap 128m --cpus 0.5 --pids-limit 32 \
|
||||
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=8m \
|
||||
--mount "type=bind,src=$data_root,dst=/var/lib/qinglong3" \
|
||||
"$OPERATOR_IMAGE" "$command_name" run \
|
||||
--command-file "/var/lib/qinglong3/$command_file" \
|
||||
>"$data_root/results/$command_file.result.json"
|
||||
}
|
||||
|
||||
run_operator setup setup.json
|
||||
grep -q '"status":"prepared"' "$data_root/results/setup.json.result.json" || fail 'fresh setup did not report prepared'
|
||||
run_operator owner owner-provision.json
|
||||
grep -q '"status":"inserted"' "$data_root/results/owner-provision.json.result.json" || fail 'Owner credential provisioning did not report inserted'
|
||||
run_operator owner owner-challenge.json
|
||||
grep -q '"status":"inserted"' "$data_root/results/owner-challenge.json.result.json" || fail 'Owner challenge did not report inserted'
|
||||
run_operator owner owner-claim.json
|
||||
grep -q '"status":"inserted"' "$data_root/results/owner-claim.json.result.json" || fail 'Owner claim did not report inserted'
|
||||
grep -q '"role":"owner"' "$data_root/results/owner-claim.json.result.json" || fail 'Owner claim did not establish the owner role'
|
||||
|
||||
ready=0
|
||||
cleanup() {
|
||||
if [ "$ready" -ne 1 ]; then
|
||||
docker rm --force "$container_name" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
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 \
|
||||
--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)
|
||||
printf '%s\n' "$container_id" >"$data_root/container.id"
|
||||
chmod 0600 "$data_root/container.id"
|
||||
|
||||
attempt=0
|
||||
while [ "$attempt" -lt 45 ]; do
|
||||
if docker logs "$container_name" 2>&1 | grep -q '"event":"active"'; then
|
||||
ready=1
|
||||
break
|
||||
fi
|
||||
running=$(docker inspect --format '{{.State.Running}}' "$container_name" 2>/dev/null || true)
|
||||
[ "$running" = true ] || fail 'application stopped before becoming active'
|
||||
attempt=$((attempt + 1))
|
||||
sleep 1
|
||||
done
|
||||
[ "$ready" -eq 1 ] || fail 'application did not become active within 45 seconds'
|
||||
umask "$old_umask"
|
||||
|
||||
printf '%s\n' \
|
||||
"QingLong 3.0 Local Alpha is active ($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.'
|
||||
Reference in New Issue
Block a user