feat(hololake): bind release chain to public path namespace
This commit is contained in:
parent
ba80036282
commit
9006075310
13 changed files with 223 additions and 29 deletions
|
|
@ -15,6 +15,14 @@ The service listens only on `127.0.0.1`. Public HTTPS routing, updater trust-key
|
|||
provisioning, artifact upload, activation, and desktop rollout are independent
|
||||
deployment gates.
|
||||
|
||||
The registered public namespace is `/hololake/releases`. The loopback origin
|
||||
accepts the manifest at both its operator health-check path `/latest.json` and
|
||||
the public updater path `/hololake/releases/latest.json`; release package URLs
|
||||
must remain under that same public prefix. The front-door proxy must therefore
|
||||
preserve the full request URI. Its loopback tunnel port remains deliberately
|
||||
unassigned until the BS-GZ-006 route owner returns the live, non-conflicting
|
||||
topology.
|
||||
|
||||
`operator.mjs` supplies the separate, root-operated verification and activation
|
||||
boundary. `verify` reconstructs a private candidate tree and accepts it only when
|
||||
the broadcast, pipeline receipt, package bytes, Developer ID receipt, Apple
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ Environment=HOLOLAKE_RELEASE_HOST=127.0.0.1
|
|||
Environment=HOLOLAKE_RELEASE_PORT=3940
|
||||
Environment=HOLOLAKE_RELEASE_STATE_ROOT=/var/lib/guanghu/hololake-release-broadcast
|
||||
Environment=HOLOLAKE_RELEASE_ENFORCE_ROOT_OWNER=1
|
||||
Environment=HOLOLAKE_RELEASE_PUBLIC_PREFIX=/hololake/releases
|
||||
ExecStart=/usr/bin/node /opt/guanghu/hololake-release-broadcast-candidate/current/server.mjs
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import fs from 'node:fs'
|
|||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { isMainModule, loadRuntimeState } from './server.mjs'
|
||||
import { DEFAULT_PUBLIC_PREFIX, isMainModule, loadRuntimeState, normalizePublicPrefix } from './server.mjs'
|
||||
|
||||
const MAX_CONTROL_BYTES = 1024 * 1024
|
||||
const SHA256_PATTERN = /^[a-f0-9]{64}$/
|
||||
|
|
@ -54,7 +54,7 @@ function directFile(directory, name, code) {
|
|||
return resolved
|
||||
}
|
||||
|
||||
function packageNameFromUrl(value) {
|
||||
function packageNameFromUrl(value, publicPrefix) {
|
||||
const url = new URL(requireText(value, 'HOLOLAKE_RELEASE_OPERATOR_PACKAGE_URL_REQUIRED'))
|
||||
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {
|
||||
fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_URL_INVALID')
|
||||
|
|
@ -66,6 +66,9 @@ function packageNameFromUrl(value) {
|
|||
fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_URL_INVALID')
|
||||
}
|
||||
if (!packageName || packageName === '.' || packageName === '..') fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_NAME_INVALID')
|
||||
if (!url.pathname.startsWith(`${publicPrefix}/`) || url.pathname === `${publicPrefix}/latest.json` || url.pathname === `${publicPrefix}/health`) {
|
||||
fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_PUBLIC_PREFIX_MISMATCH')
|
||||
}
|
||||
return packageName
|
||||
}
|
||||
|
||||
|
|
@ -75,8 +78,13 @@ function addArtifact(artifacts, source, name, type) {
|
|||
artifacts.set(name, { source, name, type })
|
||||
}
|
||||
|
||||
export function inspectReleaseBundle(sourceDirectory, humanApprovalPath, { enforceRootOwner = false } = {}) {
|
||||
export function inspectReleaseBundle(
|
||||
sourceDirectory,
|
||||
humanApprovalPath,
|
||||
{ enforceRootOwner = false, publicPrefix = DEFAULT_PUBLIC_PREFIX } = {},
|
||||
) {
|
||||
const source = path.resolve(sourceDirectory)
|
||||
const normalizedPublicPrefix = normalizePublicPrefix(publicPrefix)
|
||||
requireTrustedDirectory(source, { enforceRootOwner })
|
||||
const latestPath = directFile(source, 'latest.json', 'HOLOLAKE_RELEASE_OPERATOR_BROADCAST_PATH_INVALID')
|
||||
const pipelinePath = directFile(source, 'pipeline-receipt.json', 'HOLOLAKE_RELEASE_OPERATOR_PIPELINE_PATH_INVALID')
|
||||
|
|
@ -125,7 +133,7 @@ export function inspectReleaseBundle(sourceDirectory, humanApprovalPath, { enfor
|
|||
addArtifact(artifacts, pipelinePath, 'pipeline-receipt.json', 'control')
|
||||
addArtifact(artifacts, approvalPath, 'human-approval.json', 'control')
|
||||
for (const platform of Object.values(broadcast.platforms)) {
|
||||
const packageName = packageNameFromUrl(platform?.url)
|
||||
const packageName = packageNameFromUrl(platform?.url, normalizedPublicPrefix)
|
||||
const packagePath = directFile(source, packageName, 'HOLOLAKE_RELEASE_OPERATOR_PACKAGE_PATH_INVALID')
|
||||
const packageInfo = requireTrustedFile(packagePath, { enforceRootOwner })
|
||||
if (!Number.isSafeInteger(platform.size) || platform.size <= 0 || platform.size !== packageInfo.size) {
|
||||
|
|
@ -166,6 +174,7 @@ export function inspectReleaseBundle(sourceDirectory, humanApprovalPath, { enfor
|
|||
sourceCommit: pipelineReceipt.sourceCommit,
|
||||
broadcastSha256,
|
||||
approvalId,
|
||||
publicPrefix: normalizedPublicPrefix,
|
||||
artifacts: [...artifacts.values()],
|
||||
}
|
||||
}
|
||||
|
|
@ -235,7 +244,7 @@ function prepareCandidate(candidateRoot, plan, { enforceRootOwner, trustedGroupI
|
|||
}
|
||||
fs.chownSync(activationPath, 0, trustedGroupId)
|
||||
}
|
||||
const state = loadRuntimeState(candidateRoot, { enforceRootOwner })
|
||||
const state = loadRuntimeState(candidateRoot, { enforceRootOwner, publicPrefix: plan.publicPrefix })
|
||||
if (state.state !== 'READY_SIGNED_NOTARIZED_BROADCAST') {
|
||||
fail(`HOLOLAKE_RELEASE_OPERATOR_CANDIDATE_INVALID:${state.reasonCode || state.state}`)
|
||||
}
|
||||
|
|
@ -253,12 +262,12 @@ function requireExpected(plan, expected) {
|
|||
}
|
||||
}
|
||||
|
||||
function replaceActivation(stateRoot, candidateActivation, previousBytes, enforceRootOwner, trustedGroupId) {
|
||||
function replaceActivation(stateRoot, candidateActivation, previousBytes, enforceRootOwner, trustedGroupId, publicPrefix) {
|
||||
const tempActivation = path.join(stateRoot, `.ACTIVE.${crypto.randomUUID()}.tmp`)
|
||||
copyFileNoFollow(candidateActivation, tempActivation, enforceRootOwner ? 0o640 : 0o600)
|
||||
if (enforceRootOwner) fs.chownSync(tempActivation, 0, trustedGroupId)
|
||||
fs.renameSync(tempActivation, path.join(stateRoot, 'ACTIVE.json'))
|
||||
const committed = loadRuntimeState(stateRoot, { enforceRootOwner })
|
||||
const committed = loadRuntimeState(stateRoot, { enforceRootOwner, publicPrefix })
|
||||
if (committed.state === 'READY_SIGNED_NOTARIZED_BROADCAST') return committed
|
||||
const failedActive = path.join(stateRoot, 'ACTIVE.json')
|
||||
if (previousBytes === null) {
|
||||
|
|
@ -312,7 +321,14 @@ export function verifyReleaseBundle(sourceDirectory, humanApprovalPath) {
|
|||
}
|
||||
}
|
||||
|
||||
export function activateRelease({ sourceDirectory, humanApprovalPath, stateRoot, expected, enforceRootOwner = true }) {
|
||||
export function activateRelease({
|
||||
sourceDirectory,
|
||||
humanApprovalPath,
|
||||
stateRoot,
|
||||
expected,
|
||||
enforceRootOwner = true,
|
||||
publicPrefix = DEFAULT_PUBLIC_PREFIX,
|
||||
}) {
|
||||
if (enforceRootOwner && process.getuid?.() !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_ROOT_REQUIRED')
|
||||
const root = path.resolve(stateRoot)
|
||||
const rootInfo = requireTrustedDirectory(root, { enforceRootOwner })
|
||||
|
|
@ -320,7 +336,7 @@ export function activateRelease({ sourceDirectory, humanApprovalPath, stateRoot,
|
|||
const lockDirectory = acquireOperatorLock(root, enforceRootOwner, trustedGroupId)
|
||||
let candidateRoot = null
|
||||
try {
|
||||
const plan = inspectReleaseBundle(sourceDirectory, humanApprovalPath, { enforceRootOwner })
|
||||
const plan = inspectReleaseBundle(sourceDirectory, humanApprovalPath, { enforceRootOwner, publicPrefix })
|
||||
requireExpected(plan, expected)
|
||||
candidateRoot = fs.mkdtempSync(path.join(root, '.candidate-'))
|
||||
const candidate = prepareCandidate(candidateRoot, plan, { enforceRootOwner, trustedGroupId })
|
||||
|
|
@ -338,7 +354,14 @@ export function activateRelease({ sourceDirectory, humanApprovalPath, stateRoot,
|
|||
fs.renameSync(candidate.releaseDirectory, finalRelease)
|
||||
let committed
|
||||
try {
|
||||
committed = replaceActivation(root, candidate.activationPath, previousBytes, enforceRootOwner, trustedGroupId)
|
||||
committed = replaceActivation(
|
||||
root,
|
||||
candidate.activationPath,
|
||||
previousBytes,
|
||||
enforceRootOwner,
|
||||
trustedGroupId,
|
||||
plan.publicPrefix,
|
||||
)
|
||||
} catch (error) {
|
||||
fs.rmSync(finalRelease, { recursive: true, force: true })
|
||||
throw error
|
||||
|
|
@ -351,6 +374,7 @@ export function activateRelease({ sourceDirectory, humanApprovalPath, stateRoot,
|
|||
sourceCommit: plan.sourceCommit,
|
||||
broadcastSha256: plan.broadcastSha256,
|
||||
approvalId: plan.approvalId,
|
||||
publicPrefix: plan.publicPrefix,
|
||||
automaticUpload: false,
|
||||
automaticActivation: false,
|
||||
automaticRestart: false,
|
||||
|
|
@ -386,6 +410,7 @@ export function main(argv = process.argv.slice(2)) {
|
|||
sourceDirectory,
|
||||
humanApprovalPath,
|
||||
stateRoot: requireText(values['state-root'], 'HOLOLAKE_RELEASE_OPERATOR_STATE_ROOT_REQUIRED'),
|
||||
publicPrefix: values['public-prefix'] || DEFAULT_PUBLIC_PREFIX,
|
||||
expected: {
|
||||
releaseId: values['expect-release-id'],
|
||||
version: values['expect-version'],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"schema": "hololake.release-public-route/v1",
|
||||
"state": "REGISTERED_PENDING_FRONT_DOOR_DEPLOYMENT",
|
||||
"publicBaseUrl": "https://guanghulab.com/hololake/releases",
|
||||
"releaseEndpoint": "https://guanghulab.com/hololake/releases/latest.json",
|
||||
"publicPathPrefix": "/hololake/releases",
|
||||
"originNode": "JD-FD-PRIMARY",
|
||||
"originListener": "127.0.0.1:3940",
|
||||
"frontDoorNode": "BS-GZ-006",
|
||||
"frontDoorLoopbackPort": null,
|
||||
"proxyRequestUriPolicy": "PRESERVE_FULL_PUBLIC_PATH",
|
||||
"allowedMethods": ["GET", "HEAD"],
|
||||
"requestBodyAllowed": false,
|
||||
"latestCachePolicy": "NO_STORE",
|
||||
"packageCachePolicy": "PUBLIC_IMMUTABLE",
|
||||
"automaticUpload": false,
|
||||
"automaticActivation": false,
|
||||
"deployed": false
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import path from 'node:path'
|
|||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const MAX_CONTROL_BYTES = 1024 * 1024
|
||||
export const DEFAULT_PUBLIC_PREFIX = '/hololake/releases'
|
||||
const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
|
||||
const SHA256_PATTERN = /^[a-f0-9]{64}$/
|
||||
|
||||
|
|
@ -21,6 +22,18 @@ const requireText = (value, code) => {
|
|||
return value.trim()
|
||||
}
|
||||
|
||||
export function normalizePublicPrefix(value) {
|
||||
const prefix = requireText(value, 'HOLOLAKE_RELEASE_PUBLIC_PREFIX_REQUIRED')
|
||||
if (!prefix.startsWith('/') || prefix.endsWith('/') || prefix.includes('//') || prefix.includes('?') || prefix.includes('#')) {
|
||||
fail('HOLOLAKE_RELEASE_PUBLIC_PREFIX_INVALID')
|
||||
}
|
||||
const normalized = path.posix.normalize(prefix)
|
||||
if (normalized !== prefix || normalized === '/' || normalized.split('/').some((segment) => segment === '..')) {
|
||||
fail('HOLOLAKE_RELEASE_PUBLIC_PREFIX_INVALID')
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
|
||||
function trustedRegularFile(file, { enforceRootOwner, maxBytes = null }) {
|
||||
const info = fs.lstatSync(file)
|
||||
if (!info.isFile() || info.isSymbolicLink()) fail('HOLOLAKE_RELEASE_FILE_NOT_REGULAR')
|
||||
|
|
@ -43,13 +56,16 @@ function readTrustedJson(file, options) {
|
|||
return { bytes: fs.readFileSync(file), value: JSON.parse(fs.readFileSync(file, 'utf8')) }
|
||||
}
|
||||
|
||||
function validatePlatform(platform, releaseDirectory, enforceRootOwner, sourceCommit) {
|
||||
function validatePlatform(platform, releaseDirectory, enforceRootOwner, sourceCommit, publicPrefix) {
|
||||
const url = new URL(requireText(platform?.url, 'HOLOLAKE_RELEASE_PACKAGE_URL_REQUIRED'))
|
||||
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {
|
||||
fail('HOLOLAKE_RELEASE_PACKAGE_URL_INVALID')
|
||||
}
|
||||
const packageName = decodeURIComponent(path.posix.basename(url.pathname))
|
||||
if (!packageName || packageName === '.' || packageName === '..') fail('HOLOLAKE_RELEASE_PACKAGE_NAME_INVALID')
|
||||
if (!url.pathname.startsWith(`${publicPrefix}/`) || url.pathname === `${publicPrefix}/latest.json` || url.pathname === `${publicPrefix}/health`) {
|
||||
fail('HOLOLAKE_RELEASE_PACKAGE_PUBLIC_PREFIX_MISMATCH')
|
||||
}
|
||||
const packagePath = safeRelative(releaseDirectory, packageName, 'HOLOLAKE_RELEASE_PACKAGE_PATH_INVALID')
|
||||
const packageInfo = trustedRegularFile(packagePath, { enforceRootOwner })
|
||||
if (!Number.isSafeInteger(platform.size) || platform.size <= 0 || platform.size !== packageInfo.size) {
|
||||
|
|
@ -82,10 +98,10 @@ function validatePlatform(platform, releaseDirectory, enforceRootOwner, sourceCo
|
|||
return { pathname: url.pathname, file: packagePath, size: packageInfo.size }
|
||||
}
|
||||
|
||||
function loadActiveRelease(stateRoot, enforceRootOwner) {
|
||||
function loadActiveRelease(stateRoot, enforceRootOwner, publicPrefix) {
|
||||
const activationPath = path.join(stateRoot, 'ACTIVE.json')
|
||||
if (!fs.existsSync(activationPath)) {
|
||||
return { state: 'EMPTY_FAIL_CLOSED', release: null, reasonCode: null }
|
||||
return { state: 'EMPTY_FAIL_CLOSED', release: null, reasonCode: null, publicPrefix }
|
||||
}
|
||||
const activation = readTrustedJson(activationPath, { enforceRootOwner }).value
|
||||
if (
|
||||
|
|
@ -135,7 +151,7 @@ function loadActiveRelease(stateRoot, enforceRootOwner) {
|
|||
const releaseDirectory = path.dirname(broadcastPath)
|
||||
const packages = new Map()
|
||||
for (const platform of Object.values(broadcast.platforms)) {
|
||||
const accepted = validatePlatform(platform, releaseDirectory, enforceRootOwner, pipelineReceipt.sourceCommit)
|
||||
const accepted = validatePlatform(platform, releaseDirectory, enforceRootOwner, pipelineReceipt.sourceCommit, publicPrefix)
|
||||
if (packages.has(accepted.pathname)) fail('HOLOLAKE_RELEASE_PACKAGE_ROUTE_COLLISION')
|
||||
packages.set(accepted.pathname, accepted)
|
||||
}
|
||||
|
|
@ -143,18 +159,21 @@ function loadActiveRelease(stateRoot, enforceRootOwner) {
|
|||
state: 'READY_SIGNED_NOTARIZED_BROADCAST',
|
||||
release: { releaseId, version, broadcastBytes: broadcastDocument.bytes, packages },
|
||||
reasonCode: null,
|
||||
publicPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
export function loadRuntimeState(stateRoot, { enforceRootOwner = false } = {}) {
|
||||
export function loadRuntimeState(stateRoot, { enforceRootOwner = false, publicPrefix = DEFAULT_PUBLIC_PREFIX } = {}) {
|
||||
const resolvedRoot = path.resolve(stateRoot)
|
||||
try {
|
||||
return loadActiveRelease(resolvedRoot, enforceRootOwner)
|
||||
const normalizedPrefix = normalizePublicPrefix(publicPrefix)
|
||||
return loadActiveRelease(resolvedRoot, enforceRootOwner, normalizedPrefix)
|
||||
} catch (error) {
|
||||
return {
|
||||
state: 'LOCKED_INVALID_RELEASE_EVIDENCE',
|
||||
release: null,
|
||||
reasonCode: error instanceof Error ? error.message : 'HOLOLAKE_RELEASE_UNKNOWN_FAILURE',
|
||||
publicPrefix: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -178,7 +197,9 @@ export function createReleaseServer(runtimeState) {
|
|||
return response.end()
|
||||
}
|
||||
const pathname = new URL(request.url || '/', 'http://127.0.0.1').pathname
|
||||
if (pathname === '/health') {
|
||||
const publicHealthPath = runtimeState.publicPrefix ? `${runtimeState.publicPrefix}/health` : null
|
||||
const publicLatestPath = runtimeState.publicPrefix ? `${runtimeState.publicPrefix}/latest.json` : null
|
||||
if (pathname === '/health' || pathname === publicHealthPath) {
|
||||
const status = runtimeState.state === 'LOCKED_INVALID_RELEASE_EVIDENCE' ? 503 : 200
|
||||
return jsonResponse(response, status, {
|
||||
schema: 'hololake.release-broadcast-health/v1',
|
||||
|
|
@ -187,11 +208,15 @@ export function createReleaseServer(runtimeState) {
|
|||
automaticUpload: false,
|
||||
automaticActivation: false,
|
||||
upstreamUpdateSources: [],
|
||||
...(runtimeState.publicPrefix ? {
|
||||
publicPathPrefix: runtimeState.publicPrefix,
|
||||
releaseEndpointPath: `${runtimeState.publicPrefix}/latest.json`,
|
||||
} : {}),
|
||||
...(runtimeState.release ? { releaseId: runtimeState.release.releaseId, version: runtimeState.release.version } : {}),
|
||||
...(runtimeState.reasonCode ? { reasonCode: runtimeState.reasonCode } : {}),
|
||||
})
|
||||
}
|
||||
if (pathname === '/latest.json') {
|
||||
if (pathname === '/latest.json' || pathname === publicLatestPath) {
|
||||
if (runtimeState.state === 'EMPTY_FAIL_CLOSED') {
|
||||
response.writeHead(204, { 'Cache-Control': 'no-store' })
|
||||
return response.end()
|
||||
|
|
@ -230,7 +255,11 @@ export function main(env = process.env) {
|
|||
const port = Number(env.HOLOLAKE_RELEASE_PORT || 3940)
|
||||
if (!Number.isInteger(port) || port < 1024 || port > 65535) fail('HOLOLAKE_RELEASE_PORT_INVALID')
|
||||
const stateRoot = path.resolve(env.HOLOLAKE_RELEASE_STATE_ROOT || '/var/lib/guanghu/hololake-release-broadcast')
|
||||
const runtimeState = loadRuntimeState(stateRoot, { enforceRootOwner: env.HOLOLAKE_RELEASE_ENFORCE_ROOT_OWNER === '1' })
|
||||
const publicPrefix = env.HOLOLAKE_RELEASE_PUBLIC_PREFIX || DEFAULT_PUBLIC_PREFIX
|
||||
const runtimeState = loadRuntimeState(stateRoot, {
|
||||
enforceRootOwner: env.HOLOLAKE_RELEASE_ENFORCE_ROOT_OWNER === '1',
|
||||
publicPrefix,
|
||||
})
|
||||
const server = createReleaseServer(runtimeState)
|
||||
server.on('clientError', (_error, socket) => socket.end('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n'))
|
||||
server.listen(port, host, () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue