feat(deploy): extend canon for managed EDP and Engine grants
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from 'node:crypto'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { lstat, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const platformRoot = resolve(here, '../..')
|
||||
const workspaceRoot = resolve(platformRoot, '..')
|
||||
const artifactRoot = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || join(workspaceRoot, 'deploy-artifacts'))
|
||||
const sourceArtifact = resolve(
|
||||
process.env.NODEDC_ENGINE_CREDENTIAL_SINK_RECOVERY_SOURCE
|
||||
|| '/Volumes/docker/nodedc-deploy/applied/nodedc-engine-credential-sink-20260716-001.tgz',
|
||||
)
|
||||
const [patchId = '', ...extra] = process.argv.slice(2)
|
||||
|
||||
if (extra.length || !/^engine-credential-sink-recovery-\d{8}-\d{3}$/.test(patchId)) {
|
||||
throw new Error('usage: build-engine-credential-sink-recovery-artifact.mjs <fresh-recovery-patch-id>')
|
||||
}
|
||||
|
||||
const sourceArtifactSha256 = '0a96add05fe59db8f490927f66e07a84490474a7afb3ef7de51e1d6fd96f86a2'
|
||||
const sourceManifest = 'id=engine-credential-sink-20260716-001\ncomponent=engine\ntype=app-overlay\n'
|
||||
const entries = Object.freeze([
|
||||
'nodedc-source/server/credentialPolicies/ndcPrivateNode.js',
|
||||
'nodedc-source/server/credentialSink',
|
||||
'nodedc-source/server/index.js',
|
||||
'nodedc-source/server/routes/engineAgentGateway.js',
|
||||
'nodedc-source/server/routes/engineCredentialSink.js',
|
||||
'nodedc-source/server/routes/n8n.js',
|
||||
'nodedc-source/server/routes/ndcAgentMcp.js',
|
||||
'nodedc-source/services/backend/credential-sink/docker-compose.immutable-runtime.yml',
|
||||
])
|
||||
const payloadSha256 = Object.freeze({
|
||||
'nodedc-source/server/credentialPolicies/ndcPrivateNode.js': '723874a02dc7b8a68b22ff2304431cfe64f28933cedf5f1e6cda79b2e1cf704a',
|
||||
'nodedc-source/server/credentialSink/core.js': '9f0facc41fd398fcd955cffdd486abb126cdcd756c87ffde667fcbe00e2c41d3',
|
||||
'nodedc-source/server/credentialSink/requestAuth.js': 'f8c9237c3e6f4219dee0d4f956d6e97f76f5bd7ba8a6fd0b4fb21aceffa38138',
|
||||
'nodedc-source/server/credentialSink/store.js': 'c78dc285a973b6acd8a2330f0310935ad08720b2905454492f292d235abf12c0',
|
||||
'nodedc-source/server/credentialSink/vendor/engine-credential-sink.mjs': 'b4800eead9bf94793ff1280d06e34aad6b37ef055a9d7f8d83d7fb5f9bd66d8b',
|
||||
'nodedc-source/server/index.js': 'b2b790b02839570d967a2ca68b00e2724485a99389ac9b3a589a1b22302a36b8',
|
||||
'nodedc-source/server/routes/engineAgentGateway.js': 'e3450a4e1d5318dbac37627b67804d62804e27e2032c9e90478a1e739cea6d3d',
|
||||
'nodedc-source/server/routes/engineCredentialSink.js': '9cbb69dbc8cbe6181cd5b0170fe9c4d717b0a173866ca98cba3e3766c0bab94e',
|
||||
'nodedc-source/server/routes/n8n.js': '783d822e2457d82e890f43bc00c7e33822077dc2841f0511a89ecb210fd36d48',
|
||||
'nodedc-source/server/routes/ndcAgentMcp.js': 'fbb3342b1a617b956d5b3a6a40d5111aa107c1176b4ff89c37b104995bada081',
|
||||
'nodedc-source/services/backend/credential-sink/docker-compose.immutable-runtime.yml': '944fa64b08255eb8207b93fd327aebb98ecd9400d37d25fcfa8e3a040ee44afe',
|
||||
})
|
||||
|
||||
const sourceInfo = await lstat(sourceArtifact)
|
||||
if (sourceInfo.isSymbolicLink() || !sourceInfo.isFile()) {
|
||||
throw new Error('engine_credential_sink_recovery_source_unsafe')
|
||||
}
|
||||
if (digest(await readFile(sourceArtifact)) !== sourceArtifactSha256) {
|
||||
throw new Error('engine_credential_sink_recovery_source_sha256_mismatch')
|
||||
}
|
||||
|
||||
await mkdir(artifactRoot, { recursive: true })
|
||||
const artifact = join(artifactRoot, `nodedc-${patchId}.tgz`)
|
||||
await assertArtifactTargetFresh(artifact)
|
||||
const stage = await mkdtemp(join(tmpdir(), 'nodedc-engine-credential-sink-recovery-'))
|
||||
|
||||
try {
|
||||
run('python3', [
|
||||
'-c', verifiedExtractScript(), sourceArtifact, stage,
|
||||
sourceArtifactSha256, sourceManifest, JSON.stringify(entries), JSON.stringify(payloadSha256),
|
||||
])
|
||||
await writeFile(
|
||||
join(stage, 'manifest.env'),
|
||||
`id=${patchId}\ncomponent=engine\ntype=app-overlay\n`,
|
||||
'utf8',
|
||||
)
|
||||
await writeFile(join(stage, 'files.txt'), `${entries.join('\n')}\n`, 'utf8')
|
||||
run('python3', ['-c', canonicalTarScript(), artifact, stage])
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
patchId,
|
||||
artifact,
|
||||
sha256: digest(await readFile(artifact)),
|
||||
sourceArtifact,
|
||||
sourceArtifactSha256,
|
||||
entries,
|
||||
payloadSha256,
|
||||
changed: ['manifest.env:id'],
|
||||
}, null, 2))
|
||||
} finally {
|
||||
await rm(stage, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
async function assertArtifactTargetFresh(target) {
|
||||
try {
|
||||
await lstat(target)
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT') return
|
||||
throw error
|
||||
}
|
||||
throw new Error('engine_credential_sink_recovery_artifact_already_exists')
|
||||
}
|
||||
|
||||
function verifiedExtractScript() {
|
||||
return [
|
||||
'import hashlib,json,pathlib,sys,tarfile',
|
||||
'source=pathlib.Path(sys.argv[1]); stage=pathlib.Path(sys.argv[2])',
|
||||
'expected_archive=sys.argv[3]; expected_manifest=sys.argv[4].encode()',
|
||||
'entries=json.loads(sys.argv[5]); hashes=json.loads(sys.argv[6])',
|
||||
"raw=source.read_bytes()",
|
||||
"assert hashlib.sha256(raw).hexdigest()==expected_archive, 'source-sha256'",
|
||||
"expected_files={'manifest.env','files.txt'}|{'payload/'+name for name in hashes}",
|
||||
"expected_dirs={'payload'}",
|
||||
"for name in hashes:",
|
||||
" p=pathlib.PurePosixPath('payload/'+name)",
|
||||
" expected_dirs.update(str(parent) for parent in p.parents if str(parent)!='.')",
|
||||
"with tarfile.open(source,'r:gz') as tar:",
|
||||
" members=tar.getmembers(); names={member.name for member in members}",
|
||||
" assert names==expected_files|expected_dirs, 'source-member-set'",
|
||||
" for member in members:",
|
||||
" p=pathlib.PurePosixPath(member.name)",
|
||||
" assert not p.is_absolute() and '..' not in p.parts and '\\\\' not in member.name, 'unsafe-member'",
|
||||
" assert member.isdir() if member.name in expected_dirs else member.isfile(), 'unsafe-member-type'",
|
||||
" assert tar.extractfile('manifest.env').read()==expected_manifest, 'source-manifest'",
|
||||
" expected_list=('\\n'.join(entries)+'\\n').encode()",
|
||||
" assert tar.extractfile('files.txt').read()==expected_list, 'source-files-list'",
|
||||
" for name,wanted in hashes.items():",
|
||||
" data=tar.extractfile('payload/'+name).read()",
|
||||
" assert hashlib.sha256(data).hexdigest()==wanted, 'payload-sha256:'+name",
|
||||
" target=stage/'payload'/name; target.parent.mkdir(parents=True,exist_ok=True); target.write_bytes(data)",
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function canonicalTarScript() {
|
||||
return [
|
||||
'import gzip,io,pathlib,sys,tarfile',
|
||||
'root=pathlib.Path(sys.argv[2])',
|
||||
"with open(sys.argv[1],'wb') as out:",
|
||||
" with gzip.GzipFile(filename='',mode='wb',fileobj=out,compresslevel=9,mtime=0) as gz:",
|
||||
" with tarfile.open(fileobj=gz,mode='w',format=tarfile.PAX_FORMAT) as tar:",
|
||||
" for top in ('manifest.env','files.txt','payload'):",
|
||||
" p=root/top; paths=[p]+(sorted(p.rglob('*')) if p.is_dir() else [])",
|
||||
" for x in paths:",
|
||||
" info=tar.gettarinfo(str(x),arcname=x.relative_to(root).as_posix())",
|
||||
" info.uid=info.gid=0; info.uname=info.gname='root'; info.mtime=0; info.mode=0o755 if info.isdir() else 0o644",
|
||||
" with (open(x,'rb') if info.isfile() else io.BytesIO()) as src: tar.addfile(info,src if info.isfile() else None)",
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function run(command, args) {
|
||||
const result = spawnSync(command, args, {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
maxBuffer: 128 * 1024 * 1024,
|
||||
})
|
||||
if (result.status !== 0) throw new Error(`${command}_failed:${result.stderr || result.stdout}`)
|
||||
}
|
||||
|
||||
function digest(bytes) {
|
||||
return createHash('sha256').update(bytes).digest('hex')
|
||||
}
|
||||
Reference in New Issue
Block a user