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
|
|
@ -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'],
|
||||
|
|
|
|||
Loading…
Reference in a new issue