blob: 8f99dc5e3c4ef7b81bee0c8a985df9373bb861c5 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { isAbsolute, join, resolve, sep } from 'node:path';
import test from 'node:test';
import {
assertExpectedEpochRelation,
durableStateLocations,
parseQualificationArgs,
qualificationSandboxArgs,
qualificationSandboxInvocation,
sha256File,
} from './qualify-released-cli-state-root.mjs';
const SHA_A = 'a'.repeat(64);
const SHA_B = 'b'.repeat(64);
test('parses two exact artifacts and an epoch relation', () => {
const source = resolve(tmpdir(), 'source.tgz');
const target = resolve(tmpdir(), 'target.tgz');
assert.deepEqual(
parseQualificationArgs([
'--source',
source,
'--source-sha256',
SHA_A,
'--target',
target,
'--target-sha256',
SHA_B,
'--expect-epoch-relation',
'same',
]),
{
source,
sourceSha256: SHA_A,
target,
targetSha256: SHA_B,
expectedEpochRelation: 'same',
},
);
});
test('rejects ambiguous artifact identity and unknown arguments', () => {
const target = resolve(tmpdir(), 'target.tgz');
assert.throws(
() =>
parseQualificationArgs([
'--source',
'source.tgz',
'--source-sha256',
SHA_A,
'--target',
target,
'--target-sha256',
SHA_B,
'--expect-epoch-relation',
'any',
]),
/source must be an absolute path/u,
);
assert.throws(
() =>
parseQualificationArgs([
'--source',
resolve(tmpdir(), 'source.tgz'),
'--source-sha256',
SHA_A,
'--target',
target,
'--target-sha256',
SHA_B,
'--extra',
'value',
]),
/Unknown qualification argument/u,
);
});
test('uses privilege only for mount setup and drops every privilege before Node', () => {
const args = ['--die-with-parent', '--', '/usr/bin/node'];
assert.deepEqual(
qualificationSandboxInvocation({ args, account: { uid: 1001, gid: 1002 }, useSudo: false }),
{ command: 'bwrap', args },
);
assert.deepEqual(
qualificationSandboxInvocation({ args, account: { uid: 1001, gid: 1002 }, useSudo: true }),
{
command: 'sudo',
args: [
'--non-interactive',
'--',
'/usr/bin/bwrap',
'--die-with-parent',
'--cap-add',
'CAP_SETUID',
'--cap-add',
'CAP_SETGID',
'--cap-add',
'CAP_SETPCAP',
'--',
'/usr/bin/setpriv',
'--regid',
'1002',
'--reuid',
'1001',
'--clear-groups',
'--inh-caps=-all',
'--ambient-caps=-all',
'--bounding-set=-all',
'--no-new-privs',
'/usr/bin/node',
],
},
);
assert.throws(
() =>
qualificationSandboxInvocation({ args: ['--die-with-parent'], account: {}, useSudo: true }),
/sandbox command is missing/u,
);
});
test('creates a private Host IPC temp root before mounting a scope that may live below it', () => {
const args = qualificationSandboxArgs({
innerInputPath: '/qualification/input.json',
scope: '/qualification',
sandbox: {
home: '/qualification/home',
temp: '/qualification/tmp',
passwd: '/qualification/etc/passwd',
group: '/qualification/etc/group',
environment: {
XDG_CACHE_HOME: '/qualification/home/.cache',
XDG_CONFIG_HOME: '/qualification/home/.config',
XDG_DATA_HOME: '/qualification/home/.local/share',
},
},
});
const tmpfsIndex = args.indexOf('--tmpfs');
assert.deepEqual(args.slice(tmpfsIndex, tmpfsIndex + 8), [
'--tmpfs',
'/tmp',
'--chmod',
'1777',
'/tmp',
'--bind',
'/qualification',
'/qualification',
]);
});
test('classifies and fences the expected epoch relationship', () => {
assert.equal(assertExpectedEpochRelation(74, 76, 'different'), 'different');
assert.equal(assertExpectedEpochRelation(76, 76, 'same'), 'same');
assert.equal(assertExpectedEpochRelation(76, 78, 'any'), 'different');
assert.throws(
() => assertExpectedEpochRelation(76, 78, 'same'),
/Expected same compatibility epochs/u,
);
});
test('computes the exact artifact SHA-256', () => {
const root = mkdtempSync(join(tmpdir(), 'maka-release-digest-'));
try {
const path = join(root, 'artifact.tgz');
writeFileSync(path, 'released bytes');
assert.equal(
sha256File(path),
'2f9e0acbd320f87ceff2b9d259c99ec87830fc87d99bf914cef87394294a6682',
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('durable state covers the control namespace, not only the State Root', () => {
// The access file the Host opens before its Kernel starts lives beside the
// State Root, not inside it. A golden copy scoped to the State Root alone
// restored a workspace whose control records had already moved on, so the
// transition it proved was never the one a user performs.
const scope = resolve(tmpdir(), 'qualification-scope');
const locations = durableStateLocations(scope);
assert.ok(locations.length >= 2);
assert.ok(locations.some(({ live }) => live === join(scope, 'state-root')));
assert.ok(
locations.some(({ live }) => live.endsWith(join('.cache', 'maka', 'runtime-hosts'))),
'the account-local control namespace must be captured and restored',
);
for (const { live, golden } of locations) {
assert.ok(isAbsolute(live) && isAbsolute(golden));
assert.ok(
!golden.startsWith(`${live}${sep}`),
'a golden copy must not nest inside its live path',
);
}
});
test('a workspace target replaces tarball identity instead of weakening it', () => {
const source = resolve(tmpdir(), 'source.tgz');
const repo = resolve(tmpdir(), 'checkout');
assert.deepEqual(
parseQualificationArgs([
'--source',
source,
'--source-sha256',
SHA_A,
'--target-workspace',
repo,
]),
{ source, sourceSha256: SHA_A, targetWorkspace: repo, expectedEpochRelation: 'any' },
);
// The source stays an exact published artifact either way: the point of the
// run is that state written by a real release still opens.
assert.throws(
() => parseQualificationArgs(['--target-workspace', repo]),
/--source must be an absolute path/u,
);
assert.throws(
() =>
parseQualificationArgs([
'--source',
source,
'--source-sha256',
SHA_A,
'--target-workspace',
repo,
'--target-sha256',
SHA_B,
]),
/cannot also name a tarball target/u,
);
});