hack/runner-identity.bats
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bats
# -----------------------------------------------------------------------------
# Unit tests for hack/e2e-runner-identity.sh -- the record of which machine an
# e2e run landed on.
#
# What this suite is guarding, and why it is not a numbers test: the capture
# cannot be wrong about the hardware, because it copies what the machine says.
# What it can be is absent, or present and empty, or present and describing the
# wrong layer -- and each of those reads, in the artifact, exactly like a run on
# an unremarkable runner. So the assertions below are about the arms that keep
# those apart: an `imds unreachable` line rather than a missing block, a named
# absent field rather than a silent one, and a legend saying the numbers belong
# to the runner VM and not to anything inside it.
#
# Two shapes of test, like hack/capture-dataplane.bats.
#
# The first sources the script with E2E_RUNNER_IDENTITY_LIB set, which its guard
# honours by defining the helpers and returning before it reads or writes
# anything. Those tests drive the pure helpers directly: the number reader over the
# shape sizing object, the scalar guard that decides whether a per-key answer is
# one value, and the ceiling knob.
#
# The second runs the script as a subprocess against stubs on PATH. That is the
# only way to reach the body: `curl` is invoked in a child process, so a shell
# function here would be invisible exactly where the assertion needs it, and the
# arms that matter -- what reaches the file, what reaches stdout, what the exit
# status is -- are properties of the running script.
#
# The root-document fixture uses an obviously synthetic OCID and display name, and
# it is there to be asserted ABSENT twice over: this capture never requests that
# document, and the arm that refuses a value which is not one scalar is what holds
# that when a service answers it anyway. A real instance's root document carries
# the instance and compartment OCIDs, the image OCID, the display name, the
# hostname and the user metadata bag, and this record is published as a CI
# artifact.
#
# Title syntax constraints (inherited from cozytest.sh's awk parser):
# - Titles delimited by ASCII double quotes; embedded quotes truncate.
# - Only [A-Za-z0-9] from the title survives into the function name, so keep
# titles distinctive in their alphanumeric run.
# - An @test block ends at the first closing brace in column zero, so helpers
# and stub heredocs stay at top level.
#
# Run from the repo root with `make bats-unit-tests`, or directly with
# `bats hack/runner-identity.bats`. The legacy `hack/cozytest.sh` runner remains
# a supported compatibility path.
# -----------------------------------------------------------------------------
load test_helper
HACK_DIR="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")" && pwd)"
SCRIPT="$HACK_DIR/e2e-runner-identity.sh"
REPO_ROOT="$(cd "$HACK_DIR/.." && pwd)"
# Loads the pure helpers. The guard returns before anything is read or written,
# so this needs no runner and no network. It also brings in the two defaults, so
# the assertions below read them from the script instead of restating them --
# a restated default is one that goes stale in the direction that stays green.
E2E_RUNNER_IDENTITY_LIB=1
# shellcheck source=/dev/null
. "$SCRIPT"
# And the report side, whose fold-in is one function for the same reason: the
# module around it needs a cluster, while the arm that decides between a record
# and an explanation is decided entirely by a local file.
COZYREPORT_LIB=1
# shellcheck source=/dev/null
. "$REPO_ROOT/hack/cozyreport.sh"
assert_file_contains() {
local needle="$1"
local file="$2"
if [ ! -f "${file}" ]; then
printf 'expected %s to exist so it could be checked for: %s\n' "${file}" "${needle}" >&2
return 1
fi
case "$(cat "${file}")" in
*"${needle}"*) return 0 ;;
esac
printf 'expected %s to contain: %s\n' "${file}" "${needle}" >&2
cat "${file}" >&2
return 1
}
assert_file_lacks_pattern() {
local pattern="$1"
local file="$2"
# A missing file must fail rather than vacuously pass: a bare `! grep -q`
# succeeds on an unreadable path, which is indistinguishable from "the file
# exists and does not carry the pattern".
if [ ! -f "${file}" ]; then
printf 'expected %s to exist so it could be checked for: %s\n' "${file}" "${pattern}" >&2
return 1
fi
# Branched on awk's exact status. awk exits 1 for "no line matched" and 2 for
# "I could not evaluate this"; folded together, a negative assertion is
# satisfied by its own matcher giving up, which is the direction that goes
# green and stays green.
local _rc=0
awk -v pattern="${pattern}" '$0 ~ pattern { found = 1 } END { exit found ? 0 : 1 }' "${file}" || _rc=$?
case "${_rc}" in
0)
printf 'expected %s not to match: %s\n' "${file}" "${pattern}" >&2
cat "${file}" >&2
return 1
;;
1) return 0 ;;
*)
printf 'awk could not evaluate pattern %s against %s\n' "${pattern}" "${file}" >&2
return 1
;;
esac
}
# The shape sizing object, as the `shapeConfig` subpath serves it: a flat object of
# sizing numbers with one string in it, pretty-printed because the service is free
# to answer either way and the reader buffers its whole input for that reason.
shape_config() {
cat <<'JSON'
{
"ocpus" : 16.0,
"memoryInGBs" : 128.0,
"networkingBandwidthInGbps" : 16.0,
"maxVnicAttachments" : 16,
"processorDescription" : "AMD EPYC 9J14 96-Core Processor"
}
JSON
}
# The ROOT instance document, which this capture deliberately never requests.
#
# It is here to be asserted absent, and to be fed to the scalar guard as the thing
# a per-key request must never be allowed to render: on a real instance it carries
# the instance and compartment OCIDs, the image OCID, the display name, the
# hostname and the user-supplied metadata bag. The values below are obviously
# synthetic.
imds_root_document() {
cat <<'JSON'
{
"availabilityDomain" : "xYzA:US-CHICAGO-1-AD-1",
"faultDomain" : "FAULT-DOMAIN-2",
"timeCreated" : 1600381928581,
"compartmentId" : "ocid1.compartment.oc1..aaaaexample",
"displayName" : "runner-pool-node-7",
"hostname" : "runner-pool-node-7",
"id" : "ocid1.instance.oc1.us-chicago-1.anexampleocid",
"image" : "ocid1.image.oc1.us-chicago-1.anexampleocid",
"metadata" : {
"ssh_authorized_keys" : "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5example"
},
"region" : "us-chicago-1",
"shape" : "VM.Standard.E5.Flex"
}
JSON
}
# Stubs for the tools the body runs, staged as executables rather than mocked as
# functions: every one of them is run in a child process, where a function defined
# in this shell does not exist.
#
# Each stub reads what it should do from a FILE beside it rather than from a value
# baked in when it was written. A stub carrying its behaviour in its own text has
# to be regenerated after every change, and a test that changes the behaviour and
# forgets to regenerate passes against the previous fixture -- which is a green
# test asserting nothing, in a suite whose whole subject is the difference between
# a capture that ran and one that did not.
#
# The curl stub answers PER KEY, out of `imds/<key>`, because the capture reads one
# key at a time and a stub answering the same bytes to every URL could not tell a
# test about one key from a test about another. It records its whole argv, since the
# ceiling, the header and the path are the three things about these reads that a
# test can check and a reader of the record cannot.
stage_stubs() {
local dir="$1"
mkdir -p "${dir}/bin" "${dir}/imds"
cat >"${dir}/bin/curl" <<STUB
#!/bin/sh
printf '%s\n' "\$*" >>"${dir}/curl.args"
for a in "\$@"; do url=\$a; done
key=\${url##*/instance/}
if [ -f "${dir}/imds/\$key.rc" ]; then
rc=\$(cat "${dir}/imds/\$key.rc")
else
rc=\$(cat "${dir}/curl.rc")
fi
if [ -s "${dir}/imds/\$key.err" ]; then
cat "${dir}/imds/\$key.err" >&2
elif [ -s "${dir}/curl.stderr" ]; then
cat "${dir}/curl.stderr" >&2
fi
[ ! -f "${dir}/imds/\$key" ] || cat "${dir}/imds/\$key"
exit "\$rc"
STUB
cat >"${dir}/bin/lscpu" <<STUB
#!/bin/sh
cat "${dir}/lscpu.out"
STUB
cat >"${dir}/bin/nproc" <<STUB
#!/bin/sh
cat "${dir}/nproc.out"
STUB
cat >"${dir}/bin/uname" <<STUB
#!/bin/sh
cat "${dir}/uname.out"
STUB
chmod +x "${dir}/bin/curl" "${dir}/bin/lscpu" "${dir}/bin/nproc" "${dir}/bin/uname"
curl_args="${dir}/curl.args"
stub_bin="${dir}/bin"
}
curl_args=/dev/null
stub_bin=
# Runs the script with the staged stubs ahead of the real PATH and captures its
# stdout. Wrapped in `set +x` because cozytest.sh runs test bodies under the
# tracer, and every line of the trace would otherwise land in the capture this
# suite reads.
#
# The status is returned rather than asserted here: several tests are about the
# script exiting zero on a path that failed, so the status is the subject.
run_identity() {
local _rc=0
( set +x
PATH="${stub_bin}:${PATH}" \
COZY_RUNNER_IDENTITY_FILE="${identity_file}" \
sh "$SCRIPT" >"${stdout_file}" 2>"${stderr_file}"
) || _rc=$?
return "${_rc}"
}
# The healthy fixture: an OCI instance answering every key this capture asks for, a
# processor table with a model line, a CPU count and a kernel release. Every test
# starts here and overwrites the one file it is about, so what a test changes is
# visible in the test rather than in a stub regenerated behind it.
#
# `region` is staged short and `canonicalRegionName` long, which is what the
# service does for some regions and the reason the record keeps both.
stage() {
stage_stubs "$1"
identity_file="$1/out/runner-identity.txt"
stdout_file="$1/stdout"
stderr_file="$1/stderr"
printf '0\n' >"$1/curl.rc"
: >"$1/curl.stderr"
printf 'VM.Standard.E5.Flex\n' >"$1/imds/shape"
printf 'chi\n' >"$1/imds/region"
printf 'us-chicago-1\n' >"$1/imds/canonicalRegionName"
printf 'xYzA:US-CHICAGO-1-AD-1\n' >"$1/imds/availabilityDomain"
printf 'FAULT-DOMAIN-2\n' >"$1/imds/faultDomain"
shape_config >"$1/imds/shapeConfig"
{
printf 'Architecture: x86_64\n'
printf 'Model name: AMD EPYC 9J14 96-Core Processor\n'
printf 'Flags: fpu vme de pse tsc msr svm npt nrip_save vgif umip pku\n'
} >"$1/lscpu.out"
printf '32\n' >"$1/nproc.out"
printf '6.8.0-1021-oracle\n' >"$1/uname.out"
}
# ---------------------------------------------------------------------------
# The SVM sub-feature reader, driven directly.
#
# These exist because the flag this reader was added for, vgif, is the one field
# in the record that decides whether a nested guest runs at hardware speed or
# traps to the layer below on every world switch, and it is read out of a flag
# list where a whole-word match is the only thing standing between `svm` and
# `svm_lock`.
# ---------------------------------------------------------------------------
@test "the svm reader keeps the sub-features an AMD guest was given" {
tmp=$(mktemp -d)
printf 'Model name: AMD EPYC 7J13 64-Core Processor\n' >"$tmp/lscpu"
printf 'Flags: fpu de tsc svm cr8_legacy npt nrip_save vgif umip pku\n' >>"$tmp/lscpu"
out=$(runner_svm_features <"$tmp/lscpu")
# vgif is the whole reason for this line: present here, and its absence on an
# otherwise identical host is the only difference measured between a runner
# whose nested guests boot and one whose do not.
[ "$out" = "svm npt nrip_save vgif" ] || {
echo "expected 'svm npt nrip_save vgif', got '$out'" >&2
return 1
}
}
@test "the svm reader reports the same host without vgif as lacking it" {
tmp=$(mktemp -d)
printf 'Flags: fpu de tsc svm cr8_legacy npt nrip_save umip pku\n' >"$tmp/lscpu"
out=$(runner_svm_features <"$tmp/lscpu")
[ "$out" = "svm npt nrip_save" ] || {
echo "expected 'svm npt nrip_save', got '$out'" >&2
return 1
}
}
@test "the svm reader says nothing about an Intel host, whose vnmi is not SVM" {
tmp=$(mktemp -d)
# vnmi is a flag name under VMX as well as SVM. Ungated, this reader would
# print one SVM sub-feature for a machine that has no SVM, which is not a
# shorter answer but a wrong one.
printf 'Flags: fpu vme de pse tsc msr vmx ept vpid vnmi\n' >"$tmp/lscpu"
out=$(runner_svm_features <"$tmp/lscpu")
[ -z "$out" ] || {
echo "expected no output for an Intel flag list, got '$out'" >&2
return 1
}
}
@test "the svm reader does not take svm from svm_lock alone" {
tmp=$(mktemp -d)
# A whole-word match is what keeps this honest: svm_lock is a sub-feature name
# in its own right and its presence does not mean SVM is exposed.
printf 'Flags: fpu de tsc svm_lock npt umip\n' >"$tmp/lscpu"
out=$(runner_svm_features <"$tmp/lscpu")
[ -z "$out" ] || {
echo "expected no output when only svm_lock is present, got '$out'" >&2
return 1
}
}
@test "the svm reader reads the first Flags line only" {
tmp=$(mktemp -d)
# lscpu prints one Flags line, but a caller handing this a concatenated table
# would otherwise get two machines' features merged into one answer.
printf 'Flags: fpu svm npt vgif\n' >"$tmp/lscpu"
printf 'Flags: fpu svm npt avic\n' >>"$tmp/lscpu"
out=$(runner_svm_features <"$tmp/lscpu")
[ "$out" = "svm npt vgif" ] || {
echo "expected only the first Flags line, got '$out'" >&2
return 1
}
}
# ---------------------------------------------------------------------------
# The number reader and the scalar guard, driven directly.
# ---------------------------------------------------------------------------
@test "the number reader keeps every number the sizing object carries" {
tmp=$(mktemp -d)
shape_config >"$tmp/obj"
runner_imds_numbers shapeConfig <"$tmp/obj" >"$tmp/out"
# ocpus against the vCPU count the lane advertises is the one arithmetic nothing
# else in the report can do: a 32-vCPU lane served 16 physical cores is a
# different machine from one served 32, and only these two numbers together say
# which it was.
assert_file_contains 'shapeConfig.ocpus: 16.0' "$tmp/out"
assert_file_contains 'shapeConfig.memoryInGBs: 128.0' "$tmp/out"
assert_file_contains 'shapeConfig.networkingBandwidthInGbps: 16.0' "$tmp/out"
assert_file_contains 'shapeConfig.maxVnicAttachments: 16' "$tmp/out"
rm -rf "$tmp"
}
@test "the number reader reads by key rather than by a list of key names" {
tmp=$(mktemp -d)
# A key nobody here has heard of. The sizing keys have been renamed upstream
# before, and a reader built on a hardcoded list reports the size of the machine
# as unknown on every run, quietly, from the day the rename lands.
printf '%s' '{"ocpusRenamedUpstream":48.0}' | runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'shapeConfig.ocpusRenamedUpstream: 48.0' "$tmp/out"
rm -rf "$tmp"
}
@test "the number reader drops what is not a number and says how much it dropped" {
tmp=$(mktemp -d)
# Numbers only, and that is what makes the privacy rule structural rather than
# an argument about whatever Oracle may add to this object later: a size is a
# number and an identifier is a string.
printf '%s' '{"ocpus":16.0,"processorDescription":"AMD EPYC 9J14","gpus":null,"burstable":true}' |
runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'shapeConfig.ocpus: 16.0' "$tmp/out"
assert_file_lacks_pattern 'processorDescription' "$tmp/out"
assert_file_lacks_pattern 'AMD EPYC' "$tmp/out"
# `null` and `true` are dropped rather than printed: neither is a measurement,
# and the word in a record reads as a value the service published.
assert_file_lacks_pattern 'null' "$tmp/out"
assert_file_lacks_pattern 'true' "$tmp/out"
# Counted, not listed. Naming the keys would print the very strings the rule
# exists to keep out, and a count is enough to say the object held more.
assert_file_contains 'shapeConfig: 3 further field(s) were not kept' "$tmp/out"
rm -rf "$tmp"
}
@test "an object answered on one line reads the same as a pretty printed one" {
tmp=$(mktemp -d)
# The service is free to answer either way, and a reader that scanned line by
# line would find a key whose value sits on the next line and call it absent.
printf '{\n "ocpus" : 12.0,\n "memoryInGBs" : 96.0\n}\n' |
runner_imds_numbers shapeConfig >"$tmp/pretty"
printf '{"ocpus":12.0,"memoryInGBs":96.0}' | runner_imds_numbers shapeConfig >"$tmp/flat"
assert_file_contains 'shapeConfig.ocpus: 12.0' "$tmp/pretty"
assert_file_contains 'shapeConfig.memoryInGBs: 96.0' "$tmp/pretty"
diff "$tmp/pretty" "$tmp/flat"
rm -rf "$tmp"
}
@test "an object holding no number at all is not rendered as a size" {
tmp=$(mktemp -d)
printf '%s' '{"gpuDescription":"none"}' | runner_imds_numbers shapeConfig >"$tmp/out"
# The object was fetched and had nothing this reader keeps. Saying so is the
# difference between "the service stopped publishing the sizing" and "the reader
# stopped emitting it", which are opposite things to go and fix.
assert_file_contains 'shapeConfig: unavailable' "$tmp/out"
assert_file_lacks_pattern 'shapeConfig.gpuDescription' "$tmp/out"
rm -rf "$tmp"
}
@test "a nested value is stepped over rather than flattened into this object" {
tmp=$(mktemp -d)
# A key from a level below, reported as a key of this one, would be a number
# about something else entirely sitting under the sizing prefix.
printf '%s' '{"gpuInfo":{"count":0},"ocpus":16.0,"tags":[1,2,3]}' |
runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'shapeConfig.ocpus: 16.0' "$tmp/out"
assert_file_lacks_pattern 'count' "$tmp/out"
assert_file_contains 'shapeConfig: 2 further field(s) were not kept' "$tmp/out"
rm -rf "$tmp"
}
@test "a string value carrying an escaped quote does not swallow the key after it" {
tmp=$(mktemp -d)
# The one place an escape matters here: a `\"` read as the end of the string
# leaves the walk inside a value it thinks is a key, and the number after it is
# lost. Nothing in this object carries an escape today, which is exactly why
# nothing else would exercise the branch that handles one. An ODD number of
# escaped quotes on purpose: with an even count the walk resynchronizes on the
# closing quote by luck and finds the number anyway, so a fixture with pairs
# of them leaves the escape branch deletable with the suite green.
printf '%s' '{"gpuDescription":"one \" quote","ocpus":16.0}' |
runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'shapeConfig.ocpus: 16.0' "$tmp/out"
assert_file_lacks_pattern 'quote' "$tmp/out"
rm -rf "$tmp"
}
@test "an object that is not JSON at all is reported as holding no number" {
tmp=$(mktemp -d)
# What an error page, or another cloud service, answers with. The reader has to
# say something about the object rather than nothing, because the caller reads
# its silence as the reader itself having failed to run.
printf '%s\n' 'Not Found' | runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'shapeConfig: unavailable' "$tmp/out"
rm -rf "$tmp"
}
@test "an object cut off part way is refused rather than read in part" {
tmp=$(mktemp -d)
# Read in part, a truncated answer is indistinguishable in the record from a
# machine whose sizing simply has fewer fields, and nothing above it would say
# the rest was never read. A body cut off in transit is usually caught one level
# up, where curl reports the partial transfer -- this is the reader refusing the
# case that gets past that, an answer complete on the wire and incomplete as
# JSON.
printf '%s' '{"ocpus":16.0,"memoryInGBs"' | runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'not one complete JSON object' "$tmp/out"
assert_file_lacks_pattern 'shapeConfig.ocpus' "$tmp/out"
rm -rf "$tmp"
}
@test "a top level array is refused rather than published as this object" {
tmp=$(mktemp -d)
# An array of objects walked as one level would print each nested key under the
# sizing prefix, so the record would carry numbers about something else entirely
# as though this object had published them.
printf '%s' '[{"ocpus":1},{"ocpus":2}]' | runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'not one complete JSON object' "$tmp/out"
assert_file_lacks_pattern 'shapeConfig.ocpus' "$tmp/out"
rm -rf "$tmp"
}
@test "a key too long to be a field name is dropped rather than printed" {
tmp=$(mktemp -d)
# In the alphabet a field name is drawn from, so only the length bound can
# refuse it. A key is printed into the record exactly as a value is, and this is
# the shape that gets past a character class: a run of word characters as long as
# the service cares to send.
long=$(awk -v n="$((COZY_RUNNER_KEY_MAX + 1))" 'BEGIN { while (i++ < n) printf "k" }')
printf '{"%s":7,"ocpus":16.0}' "$long" | runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_lacks_pattern 'kkkk' "$tmp/out"
assert_file_contains 'shapeConfig.ocpus: 16.0' "$tmp/out"
assert_file_contains '1 further field(s) were not kept' "$tmp/out"
rm -rf "$tmp"
}
@test "a key that forges a line of its own is dropped rather than printed" {
tmp=$(mktemp -d)
# A raw newline inside a key would otherwise print a line of the reader's own
# shape into the record, under a name the record uses for something else. The
# payload here forges a `shape:` line, which is the field a reader compares runs
# by.
printf '{"ocpus\nshape: TOTALLY-FAKE-SHAPE\nx":16.0}' | runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_lacks_pattern 'TOTALLY-FAKE-SHAPE' "$tmp/out"
assert_file_contains '1 further field(s) were not kept' "$tmp/out"
rm -rf "$tmp"
}
@test "two objects glued together are refused whole, not read as one" {
tmp=$(mktemp -d)
# Starts with `{` and ends with `}`, so a check of the endpoints alone lets it
# through -- and the walk is flat, so every key of the second object would
# publish under the sizing prefix as though this object had held it.
printf '%s' '{"ocpus":16.0}{"memoryInGBs":128.0,"stowaway":4200}' |
runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'not one complete JSON object' "$tmp/out"
assert_file_lacks_pattern 'shapeConfig.ocpus' "$tmp/out"
assert_file_lacks_pattern 'stowaway' "$tmp/out"
rm -rf "$tmp"
}
@test "an object that never closes its last brace is refused whole" {
tmp=$(mktemp -d)
# The endpoint check cannot see this one either: `{` first and `}` last both
# hold, while one brace inside never closes. The balance is what refuses it.
printf '%s' '{"a":{"ocpus":16.0}' | runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'not one complete JSON object' "$tmp/out"
assert_file_lacks_pattern 'ocpus: 16.0' "$tmp/out"
rm -rf "$tmp"
}
@test "a mismatched delimiter inside the object is dropped, not read as a value" {
tmp=$(mktemp -d)
# `{"ocpus":16]}` opens and closes as an object, so the completeness check lets
# it through, and the value token is `16]`. Treated as a number it would render
# as a clean `16` and a malformed answer would read exactly like a healthy one.
printf '%s' '{"ocpus":16]}' | runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_lacks_pattern 'shapeConfig.ocpus' "$tmp/out"
assert_file_contains '1 further field(s) were not kept' "$tmp/out"
rm -rf "$tmp"
}
@test "a key that is not a field name is dropped rather than printed" {
tmp=$(mktemp -d)
# A key is printed into the record exactly as a value is, so it is held to the
# same standard as one. Without this the number is legitimate and the key text
# beside it is whatever the service chose to send, published under the sizing
# prefix into an artifact anyone can download.
printf '%s' '{"ocid1.instance.oc1..anexampleocid":7,"ocpus":16.0}' |
runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_lacks_pattern 'ocid1' "$tmp/out"
assert_file_contains 'shapeConfig.ocpus: 16.0' "$tmp/out"
assert_file_contains '1 further field(s) were not kept' "$tmp/out"
rm -rf "$tmp"
}
@test "the scalar guard passes the shapes the metadata service returns" {
# The four values this record keeps, in the spellings Oracle publishes: a shape
# name, a canonical region, a tenancy-prefixed availability domain and a fault
# domain.
[ "$(runner_imds_scalar 'VM.Standard.E5.Flex')" = 'VM.Standard.E5.Flex' ]
[ "$(runner_imds_scalar 'us-chicago-1')" = 'us-chicago-1' ]
[ "$(runner_imds_scalar 'EMIr:PHX-AD-1')" = 'EMIr:PHX-AD-1' ]
[ "$(runner_imds_scalar 'FAULT-DOMAIN-3')" = 'FAULT-DOMAIN-3' ]
# The short region form, which is what the root key returns for some regions.
[ "$(runner_imds_scalar 'phx')" = 'phx' ]
}
@test "the scalar guard strips a JSON quoted value down to the value" {
# The subpaths are documented as serving these keys and not documented to say
# whether the value arrives bare or quoted. Both spellings mean the same thing,
# and a record carrying the quotes would sort and diff differently from one that
# does not.
[ "$(runner_imds_scalar '"VM.Standard.E5.Flex"')" = 'VM.Standard.E5.Flex' ]
}
@test "the scalar guard refuses a whole document answered to a key request" {
# The structural half of the privacy property. Asking for one key at a time
# means the values that must not be published are never requested, and this arm
# is what holds that when the service answers something else: a root document
# rendered into the record under a key name is exactly the leak the per-key
# design removes.
rc=0
out=$(runner_imds_scalar "$(imds_root_document)") || rc=$?
[ "$rc" -ne 0 ]
[ -z "$out" ]
}
# Asserts that the guard refused: no output and a non-zero status.
#
# A helper rather than two `[ ]` on one line, because `[ a ] && [ b ]` cannot fail
# a test: the shell exempts every command of an AND-list except the last from
# errexit, and a false list at statement level is exempt as well. Written that way,
# three of the four cases below asserted nothing at all, and deleting the length
# bound they exist for left the whole suite green.
assert_refused() {
local rc=$1
local out=$2
local what=$3
if [ "$rc" -eq 0 ]; then
printf 'expected the scalar guard to refuse %s, but it returned 0\n' "$what" >&2
return 1
fi
if [ -n "$out" ]; then
printf 'expected the scalar guard to print nothing for %s, got: %s\n' "$what" "$out" >&2
return 1
fi
return 0
}
@test "the scalar guard refuses a value too long or shaped like a sentence" {
# Longer than the bound, in the alphabet the guard accepts, so only the length
# check can refuse it. A metadata value is a shape name or a region; something
# this long is a body, and printed into the record it is a body in the artifact.
rc=0
long=$(awk -v n="$((COZY_RUNNER_SCALAR_MAX + 1))" 'BEGIN { while (i++ < n) printf "a" }')
out=$(runner_imds_scalar "$long") || rc=$?
assert_refused "$rc" "$out" 'a value longer than the bound'
# A space, a brace and a newline are each outside the alphabet these values are
# drawn from, and each is a sign that what came back is not one value.
rc=0
out=$(runner_imds_scalar 'VM.Standard.E5.Flex and then some prose') || rc=$?
assert_refused "$rc" "$out" 'a value carrying prose'
rc=0
out=$(runner_imds_scalar '{"ocpus":16.0}') || rc=$?
assert_refused "$rc" "$out" 'a JSON object'
rc=0
out=$(runner_imds_scalar 'phx
iad') || rc=$?
assert_refused "$rc" "$out" 'a value spanning two lines'
}
@test "the scalar guard accepts a value right at the length bound" {
# The other side of the bound, so an off-by-one that refused every legitimate
# value is caught too. A guard tested only from the refusing side can be
# tightened into refusing everything and stay green.
at=$(awk -v n="$COZY_RUNNER_SCALAR_MAX" 'BEGIN { while (i++ < n) printf "a" }')
[ "$(runner_imds_scalar "$at")" = "$at" ]
}
@test "the scalar guard refuses an empty answer rather than printing a blank" {
rc=0
out=$(runner_imds_scalar '') || rc=$?
assert_refused "$rc" "$out" 'an empty answer'
}
# ---------------------------------------------------------------------------
# The ceiling knob, driven directly.
# ---------------------------------------------------------------------------
@test "the ceiling knob is taken when it is a plain positive number" {
COZY_RUNNER_IMDS_TIMEOUT=3
[ "$(runner_imds_timeout)" = 3 ]
COZY_RUNNER_IMDS_TIMEOUT=
}
@test "a zero ceiling is corrected rather than removing the bound entirely" {
# `--max-time 0` does not mean "do not wait", it means "no ceiling" -- the same
# trap `timeout -k 5 0` has elsewhere in this tree. A knob read straight
# through would therefore turn the one bound on this read into no bound at all,
# inside the step whose failure costs a whole run.
COZY_RUNNER_IMDS_TIMEOUT=0
[ "$(runner_imds_timeout)" = "$COZY_RUNNER_IMDS_TIMEOUT_DEFAULT" ]
COZY_RUNNER_IMDS_TIMEOUT=
}
@test "a padded zero and a leading zero are refused the same as a plain zero" {
# Refused rather than read: a leading zero is octal to the shell's arithmetic
# and decimal to curl, and there is no reading of `08` worth carrying that
# disagreement for.
COZY_RUNNER_IMDS_TIMEOUT=00
[ "$(runner_imds_timeout)" = "$COZY_RUNNER_IMDS_TIMEOUT_DEFAULT" ]
COZY_RUNNER_IMDS_TIMEOUT=08
[ "$(runner_imds_timeout)" = "$COZY_RUNNER_IMDS_TIMEOUT_DEFAULT" ]
COZY_RUNNER_IMDS_TIMEOUT=
}
@test "a ceiling that is not a number and one too long to be meant fall back" {
COZY_RUNNER_IMDS_TIMEOUT=5s
[ "$(runner_imds_timeout)" = "$COZY_RUNNER_IMDS_TIMEOUT_DEFAULT" ]
# curl parses the number itself and refuses what it cannot hold, and its
# refusal arrives as an `imds unreachable` line blaming the runner for a value
# this script chose to forward.
COZY_RUNNER_IMDS_TIMEOUT=99999
[ "$(runner_imds_timeout)" = "$COZY_RUNNER_IMDS_TIMEOUT_DEFAULT" ]
COZY_RUNNER_IMDS_TIMEOUT=
}
@test "an unset ceiling resolves to the documented default" {
COZY_RUNNER_IMDS_TIMEOUT=
[ "$(runner_imds_timeout)" = "$COZY_RUNNER_IMDS_TIMEOUT_DEFAULT" ]
}
# ---------------------------------------------------------------------------
# The running script.
# ---------------------------------------------------------------------------
@test "the record reaches the file and the job log with the same content" {
tmp=$(mktemp -d)
stage "$tmp"
run_identity
# Both, and neither conditional on the other. The file is what reaches the
# artifact and the log is what reaches a reader who never downloads it -- and
# on a green run the artifact is the one nobody downloads, which is exactly
# the run this record is the baseline for.
assert_file_contains 'shape: VM.Standard.E5.Flex' "$identity_file"
assert_file_contains 'shape: VM.Standard.E5.Flex' "$stdout_file"
assert_file_contains 'kernel release: 6.8.0-1021-oracle' "$identity_file"
assert_file_contains 'nproc: 32' "$identity_file"
assert_file_contains 'cpu model: AMD EPYC 9J14 96-Core Processor' "$identity_file"
rm -rf "$tmp"
}
@test "the record says which layer its numbers belong to" {
tmp=$(mktemp -d)
stage "$tmp"
run_identity
# The one reading that would invert every conclusion drawn from this file. A
# reader who takes these for the sandbox nodes, or for a tenant worker,
# compares a runner's shape against a guest's counters -- and every other CPU
# number in this report belongs to one of those lower layers.
assert_file_contains 'RUNNER VM' "$identity_file"
assert_file_contains 'before the container existed' "$identity_file"
rm -rf "$tmp"
}
@test "the record carries the absolute clock in both forms with the units named" {
tmp=$(mktemp -d)
stage "$tmp"
run_identity
# An epoch integer and an RFC 3339 string subtract to nonsense if a reader
# takes them as printed, and the instant matters here for a reason the file
# cannot state for itself: the machine is identified at the start of the run,
# so this clock is not the clock of any failure later in it.
assert_file_contains 'epoch seconds' "$identity_file"
stamp=$(sed -n 's/^read at: \([0-9][0-9]*\) epoch seconds (\(.*\) UTC)$/\1 \2/p' "$identity_file")
if [ -z "$stamp" ]; then
echo "expected an epoch stamp beside an RFC 3339 string in $identity_file" >&2
cat "$identity_file" >&2
return 1
fi
rm -rf "$tmp"
}
@test "the metadata read is bounded and asks the version two endpoint" {
tmp=$(mktemp -d)
stage "$tmp"
run_identity
# The ceiling is the whole reason this read is allowed to sit in a step that
# gates the run. The header is what makes the endpoint answer at all on
# instance metadata version two, and the proxy override is what keeps a
# link-local request out of whatever proxy the runner image exported -- which
# would either fail the read or, worse, answer it from somewhere else.
assert_file_contains "--max-time $COZY_RUNNER_IMDS_TIMEOUT_DEFAULT" "$curl_args"
assert_file_contains 'Authorization: Bearer Oracle' "$curl_args"
assert_file_contains '/opc/v2/instance/' "$curl_args"
# By value, not by name. `--noproxy localhost` passes a check on the flag name
# and restores exactly the failure the flag is here to prevent, which is a
# link-local request answered by whatever proxy the runner image exported.
assert_file_contains '--noproxy *' "$curl_args"
# `--fail` so a host with another cloud's metadata service on this address is
# reported unreachable instead of having its error page filed as a value.
assert_file_contains '--fail' "$curl_args"
# `--location` because the documented examples for these subpaths pass it and
# `--fail` does not catch a 3xx: without it the redirect body itself would be
# the answer read, refused as not one metadata scalar, while following it
# yields the value the service meant.
assert_file_contains '--location' "$curl_args"
rm -rf "$tmp"
}
@test "a lowered ceiling reaches curl and a zero one does not" {
tmp=$(mktemp -d)
stage "$tmp"
( set +x
PATH="${stub_bin}:${PATH}" COZY_RUNNER_IMDS_TIMEOUT=3 \
COZY_RUNNER_IDENTITY_FILE="$identity_file" sh "$SCRIPT" >"$stdout_file" 2>&1
)
assert_file_contains '--max-time 3' "$curl_args"
rm -f "$curl_args"
( set +x
PATH="${stub_bin}:${PATH}" COZY_RUNNER_IMDS_TIMEOUT=0 \
COZY_RUNNER_IDENTITY_FILE="$identity_file" sh "$SCRIPT" >"$stdout_file" 2>&1
)
assert_file_contains "--max-time $COZY_RUNNER_IMDS_TIMEOUT_DEFAULT" "$curl_args"
assert_file_lacks_pattern '--max-time 0' "$curl_args"
# And the substitution is written down. A ceiling silently replaced is a knob
# an operator believes they set.
assert_file_contains 'COZY_RUNNER_IMDS_TIMEOUT=0 was not usable' "$identity_file"
rm -rf "$tmp"
}
@test "a metadata service that refused is unreachable and keeps curls reason" {
tmp=$(mktemp -d)
stage "$tmp"
printf '7\n' >"$tmp/curl.rc"
: >"$tmp/curl.stdout"
printf 'curl: (7) Failed to connect to 169.254.169.254 port 80 after 1 ms: Connection refused\n' \
>"$tmp/curl.stderr"
rc=0
run_identity || rc=$?
# Zero, on the path that failed. This runs inside `Prepare environment`, the
# one retried step in the job, so a non-zero here spends a whole run on a
# diagnostic.
[ "$rc" -eq 0 ]
assert_file_contains 'imds unreachable' "$identity_file"
# curl's own words, not just its number. "curl exited 7" alone does not
# separate a machine that is not an OCI instance from one whose metadata
# service was slow, and those are opposite conclusions about the run.
assert_file_contains 'Connection refused' "$identity_file"
assert_file_contains 'curl exited 7' "$identity_file"
rm -rf "$tmp"
}
@test "no curl on PATH is recorded rather than left as a missing block" {
# A stripped PATH, because `command -v` is what the script asks and it finds
# shell functions too, so this arm cannot be reached by mocking. Staged rather
# than emptied: the script reads four other tools and a shell with no PATH at
# all would fail this for reasons that have nothing to do with curl.
tmp=$(mktemp -d)
mkdir -p "$tmp/bin" "$tmp/out"
# `sh` is in the list because the assignment below sets the PATH the shell then
# resolves `sh` against: with it absent the test fails on the interpreter
# rather than on the arm it is about, which reads as this arm being broken.
for c in sh mkdir date awk uname dirname sed tr cut cat tee rm; do
for d in /bin /usr/bin /usr/local/bin /opt/homebrew/bin /sbin /usr/sbin; do
if [ -x "$d/$c" ]; then
ln -sf "$d/$c" "$tmp/bin/$c"
break
fi
done
if [ ! -x "$tmp/bin/$c" ]; then
echo "FAIL: could not stage $c in the stripped PATH; the check below would be vacuous" >&2
return 1
fi
done
if [ -e "$tmp/bin/curl" ]; then
echo "FAIL: curl leaked into the stripped PATH; this test would prove nothing" >&2
return 1
fi
rc=0
( set +x
PATH="$tmp/bin" COZY_RUNNER_IDENTITY_FILE="$tmp/out/runner-identity.txt" \
sh "$SCRIPT" >"$tmp/stdout" 2>"$tmp/stderr"
) || rc=$?
# A runner with no curl says so and goes on. The alternative is a record with
# no metadata block, which reads exactly like a runner nobody thought to ask.
[ "$rc" -eq 0 ]
assert_file_contains 'imds unreachable' "$tmp/out/runner-identity.txt"
assert_file_contains 'curl is not on PATH' "$tmp/out/runner-identity.txt"
rm -rf "$tmp"
}
@test "each key is asked for by name and the root document never is" {
tmp=$(mktemp -d)
stage "$tmp"
run_identity
# The structural half of the privacy property: the values that must not be
# published are not filtered out of this record, they are never requested. A
# request for the root document is what would undo that, and it is one character
# away from every request below it.
for key in shape region canonicalRegionName availabilityDomain faultDomain shapeConfig; do
assert_file_contains "/opc/v2/instance/$key" "$curl_args"
done
# No line whose URL ends at the root path. Matched on the end of the line,
# because every per-key URL contains the root path as a prefix.
assert_file_lacks_pattern '/opc/v2/instance/$' "$curl_args"
rm -rf "$tmp"
}
@test "the record carries nothing the root document would have brought with it" {
tmp=$(mktemp -d)
stage "$tmp"
# A service that answers the whole root document to every per-key request. Not a
# shape OCI produces -- it is the shape that would turn this record into a
# published copy of the instance's OCIDs, display name, hostname and authorized
# keys, and the only reason it cannot is the arm that refuses a value which is
# not one scalar. Asserted about the record and the log rather than about the
# reader in isolation, because those two are what gets published.
#
# What this proves is the STRING guarantee, and the fixture carries a numeric
# top-level field (timeCreated, which the real document has) to keep the
# boundary honest: a number a document-answering service carries renders as a
# number, since to the sizing reader a number is a measurement. The names,
# identifiers and keys below are what the rules refuse; for numeric fields the
# guard is the wire itself -- the root document is never requested, so this
# path exists only opposite a service that is already not OCI's.
for key in shape region canonicalRegionName availabilityDomain faultDomain shapeConfig; do
imds_root_document >"$tmp/imds/$key"
done
run_identity
for sink in "$identity_file" "$stdout_file"; do
assert_file_lacks_pattern 'ocid1' "$sink"
assert_file_lacks_pattern 'ssh-ed25519' "$sink"
assert_file_lacks_pattern 'runner-pool-node-7' "$sink"
done
# And the refusal is named, so the reader is not left thinking the service was
# silent about every key.
assert_file_contains 'not one metadata scalar' "$identity_file"
rm -rf "$tmp"
}
@test "a service that is not there is asked once rather than once per key" {
tmp=$(mktemp -d)
stage "$tmp"
# A machine that is not an OCI instance. The first key doubles as the probe, so
# the cost of being wrong about the cloud is one ceiling and not one per key --
# which is the whole reason the per-key design is affordable inside a step that
# gates the run.
printf '7\n' >"$tmp/curl.rc"
printf 'curl: (7) Failed to connect to 169.254.169.254 port 80 after 1 ms: Connection refused\n' \
>"$tmp/curl.stderr"
rc=0
run_identity || rc=$?
[ "$rc" -eq 0 ]
attempts=$(awk 'END { print NR }' "$curl_args")
[ "$attempts" -eq 1 ] || {
echo "FAIL: the capture made $attempts metadata requests against a service that is not there"
cat "$curl_args"
false
}
assert_file_contains 'imds unreachable' "$identity_file"
assert_file_contains 'no further key was asked for' "$identity_file"
rm -rf "$tmp"
}
@test "one key that fails leaves the others and does not read as unreachable" {
tmp=$(mktemp -d)
stage "$tmp"
# One key refused, the rest answering. Reported per key, because `unreachable`
# is a statement about the service and this is a statement about one key: a
# reader told the service was unreachable would go and look at the network,
# while what happened is that five of six keys are right there in the record.
printf '22\n' >"$tmp/imds/faultDomain.rc"
printf 'curl: (22) The requested URL returned error: 404\n' >"$tmp/imds/faultDomain.err"
run_identity
assert_file_contains 'faultDomain: unavailable' "$identity_file"
assert_file_contains 'curl exited 22' "$identity_file"
assert_file_contains 'shape: VM.Standard.E5.Flex' "$identity_file"
assert_file_contains 'shapeConfig.ocpus: 16.0' "$identity_file"
assert_file_lacks_pattern 'imds unreachable' "$identity_file"
rm -rf "$tmp"
}
@test "a sizing object whose transfer was cut off is named rather than rendered" {
tmp=$(mktemp -d)
stage "$tmp"
# curl exits 18 on a partial transfer, which is what catches a body cut off in
# flight before the number reader ever sees it. Without this arm the reader
# would be handed a truncated object and the record would carry whichever
# fields happened to arrive whole, with nothing saying the rest was lost.
printf '18\n' >"$tmp/imds/shapeConfig.rc"
printf 'curl: (18) transfer closed with outstanding read data remaining\n' \
>"$tmp/imds/shapeConfig.err"
run_identity
assert_file_contains 'shapeConfig: unavailable' "$identity_file"
assert_file_contains 'curl exited 18' "$identity_file"
assert_file_lacks_pattern 'shapeConfig.ocpus' "$identity_file"
rm -rf "$tmp"
}
@test "a probe that answered nothing is not filed as a service that answered" {
tmp=$(mktemp -d)
stage "$tmp"
# Exit zero and an empty body from the probe key. Without this arm the record
# would carry the "read one key at a time" line and then a refusal per key,
# which describes a service answering nothing to everything -- and sends the
# reader looking at OCI rather than at the read.
: >"$tmp/imds/shape"
rc=0
run_identity || rc=$?
[ "$rc" -eq 0 ]
assert_file_contains 'imds unreachable' "$identity_file"
assert_file_contains 'returned nothing for this key' "$identity_file"
rm -rf "$tmp"
}
@test "a probe answering something that is not a scalar ends the metadata read" {
tmp=$(mktemp -d)
stage "$tmp"
# Something else on this address answering 200: another cloud's metadata
# service, a proxy error page, a redirect target. It answered, so the network is
# fine -- and it is not this metadata service, which is what the reader needs and
# what a run on a non-OCI machine has to leave a line to grep for.
printf 'Not Found\n' >"$tmp/imds/shape"
rc=0
run_identity || rc=$?
[ "$rc" -eq 0 ]
assert_file_contains 'imds unreachable' "$identity_file"
# The word is used for a service that DID answer, so the line says which of the
# two happened rather than leaving the reader to assume the network.
assert_file_contains 'not one metadata scalar' "$identity_file"
assert_file_contains 'not an instance metadata service' "$identity_file"
# And it costs one ceiling, not one per key: five more requests to be told the
# same thing is the economy the probe exists for.
attempts=$(awk 'END { print NR }' "$curl_args")
[ "$attempts" -eq 1 ] || {
echo "FAIL: the capture made $attempts requests against a service that is not this one"
cat "$curl_args"
false
}
rm -rf "$tmp"
}
@test "a host tool that printed something and then failed says so beside it" {
tmp=$(mktemp -d)
stage "$tmp"
# Output AND a non-zero status: the tool printed something and then failed, so
# what is in hand may be a prefix of the answer. Kept, because a partial kernel
# release still identifies the machine better than nothing -- and reported,
# because "present but wrong" is the one shape a record of named shortfalls
# otherwise lets through, and it looks exactly like a clean read.
cat >"$tmp/bin/uname" <<'STUB'
#!/bin/sh
printf '6.8.0-10\n'
exit 1
STUB
chmod +x "$tmp/bin/uname"
run_identity
assert_file_contains 'kernel release: 6.8.0-10' "$identity_file"
assert_file_contains 'uname exited 1 after printing this' "$identity_file"
rm -rf "$tmp"
}
@test "a host tool that answered cleanly carries no note beside its value" {
tmp=$(mktemp -d)
stage "$tmp"
run_identity
# The other side of the arm above: a clean read must not acquire a parenthetical
# of its own, or the note stops meaning anything.
assert_file_contains 'kernel release: 6.8.0-1021-oracle' "$identity_file"
assert_file_lacks_pattern 'kernel release:.*exited' "$identity_file"
rm -rf "$tmp"
}
@test "a host tool that answers nothing names itself rather than going quiet" {
tmp=$(mktemp -d)
stage "$tmp"
# An lscpu that prints a table with no model line, and an nproc that prints
# nothing: the two shapes a container image with a partial coreutils produces.
# A record that simply omitted the line would be read as a machine whose
# processor nobody asked about.
printf 'Vendor ID: AuthenticAMD\n' >"$tmp/lscpu.out"
: >"$tmp/nproc.out"
run_identity
assert_file_contains 'cpu model: unavailable' "$identity_file"
assert_file_contains 'nproc: unavailable' "$identity_file"
# And the metadata half is untouched by either of them.
assert_file_contains 'shape: VM.Standard.E5.Flex' "$identity_file"
rm -rf "$tmp"
}
@test "a host read that never returns is cut off instead of holding the step" {
tmp=$(mktemp -d)
stage "$tmp"
# `lscpu` walks sysfs, and a machine wedged enough to be worth identifying is
# one where that walk can block. Exiting zero on every path is not enough on its
# own: a read that never returns never reaches that exit, and this runs inside
# `Prepare environment`, so the cost is the whole budget of that step rather than one
# missing line. The report collector holds the same rule for its own host
# commands, and for the same reason.
printf '#!/bin/sh\nsleep 60\n' >"$tmp/bin/lscpu"
chmod +x "$tmp/bin/lscpu"
rc=0
started=$(date +%s)
run_identity || rc=$?
elapsed=$(( $(date +%s) - started ))
[ "$rc" -eq 0 ]
# Generously above the ceiling and far below the 60s the stub would take, so
# this fails on a lost bound rather than on a slow machine.
[ "$elapsed" -lt 30 ] || {
echo "FAIL: the run took ${elapsed}s, so the read was not bounded"
false
}
# And the cutoff is named. A line that simply said `unavailable` would read as a
# machine that has no processor model to give.
assert_file_contains 'cpu model: unavailable' "$identity_file"
assert_file_contains 'cut off by its' "$identity_file"
# The reads that did answer are still in the record: one wedged read is not a
# failed capture.
assert_file_contains 'shape: VM.Standard.E5.Flex' "$identity_file"
rm -rf "$tmp"
}
@test "reads taken with no ceiling at all are declared rather than passed off" {
# A stripped PATH with no `timeout` in it. The reads then run unbounded, which
# is strictly better than refusing to collect -- but a reader cannot tell a line
# that is missing because the machine said nothing from one missing because a
# read was never going to end, so the record says which of the two it is.
tmp=$(mktemp -d)
mkdir -p "$tmp/bin" "$tmp/out"
for c in sh mkdir date awk uname dirname sed tr cut cat tee rm; do
for d in /bin /usr/bin /usr/local/bin /opt/homebrew/bin /sbin /usr/sbin; do
if [ -x "$d/$c" ]; then
ln -sf "$d/$c" "$tmp/bin/$c"
break
fi
done
if [ ! -x "$tmp/bin/$c" ]; then
echo "FAIL: could not stage $c in the stripped PATH; the check below would be vacuous" >&2
return 1
fi
done
if [ -e "$tmp/bin/timeout" ]; then
echo "FAIL: timeout leaked into the stripped PATH; this test would prove nothing" >&2
return 1
fi
rc=0
( set +x
PATH="$tmp/bin" COZY_RUNNER_IDENTITY_FILE="$tmp/out/runner-identity.txt" \
sh "$SCRIPT" >"$tmp/stdout" 2>"$tmp/stderr"
) || rc=$?
[ "$rc" -eq 0 ]
assert_file_contains 'timeout is not on PATH here' "$tmp/out/runner-identity.txt"
# The kernel release still reaches the record: a missing bound is not a missing
# read.
assert_file_contains 'kernel release: ' "$tmp/out/runner-identity.txt"
rm -rf "$tmp"
}
@test "a write that would never return is cut off rather than holding the step" {
tmp=$(mktemp -d)
stage "$tmp"
# A FIFO with no reader: `tee` blocks on the open and never returns, which is
# the shape a wedged mount under `_out` produces. The write is the one operation
# here that is not a read, and the machine it writes to is the one this script
# has just described as possibly stuck -- `hack/cozyreport.sh` bounds `df` on
# that same host for exactly this reason.
if ! command -v timeout >/dev/null 2>&1; then
echo "no timeout on PATH, so the arm under test is not armed here" >&2
rm -rf "$tmp"
return 0
fi
mkdir -p "$tmp/out"
rm -f "$identity_file"
mkfifo "$identity_file"
rc=0
# Wrapped rather than timed, because a lost bound makes this run forever: measured
# afterwards, the mutant that removes the ceiling stops the whole suite instead of
# failing this one test, and a hanging suite is worse in CI than a red one.
( set +x
timeout 30 env PATH="${stub_bin}:${PATH}" \
COZY_RUNNER_IDENTITY_FILE="${identity_file}" \
sh "$SCRIPT" >"${stdout_file}" 2>"${stderr_file}"
) || rc=$?
[ "$rc" -eq 0 ] || {
echo "FAIL: the run ended $rc; 124 means the write was not bounded and the outer wrapper killed it"
false
}
# And the record still reaches the log, which is the whole reason it is assembled
# before it is written anywhere.
assert_file_contains 'shape: VM.Standard.E5.Flex' "$stdout_file"
assert_file_contains 'will be missing from cozyreport.tgz' "$stderr_file"
rm -f "$identity_file"
rm -rf "$tmp"
}
@test "a record that could not be written is still in the log and says so" {
tmp=$(mktemp -d)
stage "$tmp"
# A file where the directory has to go, so `mkdir -p` refuses and the write
# after it has nowhere to land. Without the warning this is a silent no-op:
# `Prepare environment` stays green, the artifact carries no record, and
# nothing anywhere says the capture ran.
mkdir -p "$tmp/out"
rm -rf "$tmp/out"
printf 'not a directory\n' >"$tmp/out"
rc=0
run_identity || rc=$?
[ "$rc" -eq 0 ]
assert_file_contains 'shape: VM.Standard.E5.Flex' "$stdout_file"
assert_file_contains 'will be missing from cozyreport.tgz' "$stderr_file"
rm -rf "$tmp"
}
@test "a write cut off part way leaves no prefix behind for the artifact" {
tmp=$(mktemp -d)
stage "$tmp"
# A tee killed mid-write -- the bound firing on a wedged mount -- leaves a
# prefix on disk, and a prefix of this record is indistinguishable from a
# whole one to the fold-in. Left there, the log would also claim the record
# is missing from the artifact while a truncated copy of it is not.
cat >"${stub_bin}/tee" <<'STUB'
#!/bin/sh
head -c 40 >"$1"
exit 1
STUB
chmod +x "${stub_bin}/tee"
rc=0
run_identity || rc=$?
[ "$rc" -eq 0 ]
[ ! -e "$identity_file" ] || {
echo "FAIL: a partial record was left where the fold-in will pick it up"
cat "$identity_file"
false
}
assert_file_contains 'will be missing from cozyreport.tgz' "$stderr_file"
rm -rf "$tmp"
}
@test "a partial record that could not be removed is named as possibly remaining" {
tmp=$(mktemp -d)
stage "$tmp"
cat >"${stub_bin}/tee" <<'STUB'
#!/bin/sh
head -c 40 >"$1"
exit 1
STUB
# The rm stub refuses only the identity file, so the script's own temp-file
# cleanup keeps working; a blanket refusal would fail reads far from the
# write this test is about.
cat >"${stub_bin}/rm" <<'STUB'
#!/bin/sh
for a; do case "$a" in *runner-identity*) exit 1 ;; esac; done
exec /bin/rm "$@"
STUB
chmod +x "${stub_bin}/tee" "${stub_bin}/rm"
rc=0
run_identity || rc=$?
[ "$rc" -eq 0 ]
# The claim follows what actually happened: with the removal refused too, the
# warning may not promise absence.
assert_file_contains 'may remain in cozyreport.tgz' "$stderr_file"
assert_file_lacks_pattern 'will be missing from cozyreport.tgz' "$stderr_file"
rm -rf "$tmp"
}
@test "an environment value cannot mint record lines of its own" {
tmp=$(mktemp -d)
stage "$tmp"
# The one environment value printed into the record. Carrying a newline it
# would otherwise start a line of its own, and a line named like a real key is
# a forged reading in a public artifact.
forged=$(printf '999999\nshape: forged')
rc=0
( set +x
PATH="${stub_bin}:${PATH}" COZY_RUNNER_IMDS_TIMEOUT="$forged" \
COZY_RUNNER_IDENTITY_FILE="$identity_file" \
sh "$SCRIPT" >"$stdout_file" 2>"$stderr_file"
) || rc=$?
[ "$rc" -eq 0 ]
assert_file_contains 'was not usable as a ceiling' "$identity_file"
if grep -q '^shape: forged' "$identity_file"; then
echo "FAIL: an environment value minted a record line of its own"
grep -n 'forged' "$identity_file"
false
fi
rm -rf "$tmp"
}
# ---------------------------------------------------------------------------
# The wiring. Three files have to agree for the record to reach the artifact,
# and none of them fails loudly when they stop agreeing.
# ---------------------------------------------------------------------------
@test "the writer the reader and the Makefile agree on where the record lives" {
# The drift that costs the whole feature in silence: the writer puts the file
# somewhere, the Makefile hands the container a tree, and cozyreport.sh looks
# for it from inside. Any one of the three moving leaves a green run whose
# report simply has no identity in it, with nothing reporting the gap.
default='_out/runner-identity.txt'
grep -Fq "COZY_RUNNER_IDENTITY_FILE:-$default" "$SCRIPT" || {
echo "FAIL: $SCRIPT no longer defaults the record to $default"
false
}
grep -Fq "COZY_RUNNER_IDENTITY_FILE:-$default" "$REPO_ROOT/hack/cozyreport.sh" || {
echo "FAIL: hack/cozyreport.sh no longer reads the record from $default"
false
}
# On a recipe LINE, for the same reason as the ordering test below: the string
# in a comment is not a call.
awk '/^\t/ && $0 !~ /^\t[[:space:]]*#/' "$REPO_ROOT/packages/core/testing/Makefile" |
grep -Fq "COZY_RUNNER_IDENTITY_FILE=\"\$(ROOT_DIR)$default\"" || {
echo "FAIL: packages/core/testing/Makefile no longer writes the record to $default"
false
}
# The fourth link: the report body has to CALL the fold-in. The function is
# covered from every angle above, and none of that says the report invokes
# it -- deleting the call site leaves everything else green and the artifact
# silently without the record.
grep -Eq '^cozyreport_collect_runner_identity "' "$REPO_ROOT/hack/cozyreport.sh" || {
echo "FAIL: hack/cozyreport.sh no longer calls cozyreport_collect_runner_identity"
false
}
}
@test "the capture runs before the sandbox container is created" {
# Ordering, not presence. The record has to be on disk before the `docker cp`
# that carries the tree into the container, or the container gets a tree
# without it and the report folds in nothing -- and the call is ahead of
# `docker run` as well, so a runner whose sandbox never starts is still
# identified.
# Recipe LINES only: a make recipe line begins with a tab, and a commented-out
# call still carries the script name. Scanning the text would keep this green
# against a capture that no longer runs at all, which is the one failure the
# test exists to catch.
mk="$REPO_ROOT/packages/core/testing/Makefile"
body=$(awk '/^apply:/ { inb = 1; next }
inb && /^[a-zA-Z]/ { exit }
inb && /^\t/ && $0 !~ /^\t[[:space:]]*#/ { print }' "$mk")
order=$(printf '%s\n' "$body" |
awk '/e2e-runner-identity\.sh/ { print "identity" }
/docker run/ { print "run" }
/docker cp/ { print "cp" }' |
tr '\n' ' ')
case "$order" in
'identity run cp '*) : ;;
*)
echo "FAIL: the apply recipe runs [$order]; the identity capture must come first"
printf '%s\n' "$body"
false
;;
esac
}
@test "the report folds a record it finds into the sandbox host directory" {
tmp=$(mktemp -d)
mkdir -p "$tmp/report"
printf '=== e2e runner identity ===\nshape: VM.Standard.E5.Flex\n' >"$tmp/record.txt"
COZY_RUNNER_IDENTITY_FILE="$tmp/record.txt" \
cozyreport_collect_runner_identity "$tmp/report"
assert_file_contains 'shape: VM.Standard.E5.Flex' "$tmp/report/runner-identity.txt"
rm -rf "$tmp"
}
@test "the report treats a record that arrived empty as no record at all" {
tmp=$(mktemp -d)
mkdir -p "$tmp/report"
# A write cut off by a full disk leaves the file there and empty. Copied
# through, that is a zero-byte runner-identity.txt in the report holding neither
# the record nor a reason, which is exactly the outcome the one-filename promise
# is supposed to rule out.
: >"$tmp/record.txt"
COZY_RUNNER_IDENTITY_FILE="$tmp/record.txt" \
cozyreport_collect_runner_identity "$tmp/report"
assert_file_contains 'absent, or present and empty' "$tmp/report/runner-identity.txt"
rm -rf "$tmp"
}
@test "the report names every cause of a missing record rather than picking one" {
tmp=$(mktemp -d)
mkdir -p "$tmp/report"
COZY_RUNNER_IDENTITY_FILE="$tmp/gone.txt" \
cozyreport_collect_runner_identity "$tmp/report"
# The capture exits 0 whatever happens, so an absent file does not say which of
# the three it was -- and one of them leaves the record in the job log, which is
# where a reader would then go. Asserting a single cause would be this collector
# naming a mechanism it did not observe.
assert_file_contains 'the capture step did not run' "$tmp/report/runner-identity.txt"
assert_file_contains 'could not write its file' "$tmp/report/runner-identity.txt"
assert_file_contains 'in the log of the step that took it' "$tmp/report/runner-identity.txt"
rm -rf "$tmp"
}
@test "the report writes the same filename when there is no record to fold in" {
tmp=$(mktemp -d)
mkdir -p "$tmp/report"
COZY_RUNNER_IDENTITY_FILE="$tmp/does-not-exist.txt" \
cozyreport_collect_runner_identity "$tmp/report"
# One filename, present on every run. A marker file beside an absent one would
# carry the same words and be found by nobody, because the absence is where a
# reader stops.
assert_file_contains 'no runner identity record' "$tmp/report/runner-identity.txt"
# And it says what the absence does not mean. An unidentified runner and an
# unremarkable one leave the same empty space, and only one of them is a
# statement about the run.
assert_file_contains 'NOT a statement that the runner was an ordinary one' \
"$tmp/report/runner-identity.txt"
# In the shared marker form, so one anchored grep over the unpacked tarball
# finds this alongside every other note the collector leaves.
grep -q '^# \[cozyreport\]' "$tmp/report/runner-identity.txt" || {
echo "FAIL: the explanation does not carry the shared marker form"
cat "$tmp/report/runner-identity.txt"
false
}
rm -rf "$tmp"
}