Ctrl K
ComunidadGuías…

hack/multus-install-cni-plugins.bats

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bats
# Behavioural tests for the multus install-cni-plugins init container script:
# extracted from the rendered DaemonSet and run. The helm-unittest cases in
# packages/system/multus/tests/multus_test.yaml match its source TEXT instead.
#
# Run from the repo root through `make bats-unit-tests`; relative paths resolve
# against that cwd. The file also remains compatible with the legacy
# `hack/cozytest.sh` translator, so each @test builds and removes its fixture
# inline instead of relying on setup()/teardown().
#
# Test-level EXIT traps replace Bats' own handler and hide failing TAP results.
# Cleanup follows aborting assertions; see docs/agents/e2e-testing.md.
# Traps in the script under test run in a separate shell and remain in scope.

load test_helper

CHART=packages/system/multus

# Extract the init container's script from the rendered chart into $1 and rebind
# its two absolute paths ($2 = host cni bin, $3 = plugin source) so it can run
# unprivileged. The non-empty check must be an early `return 1`: hack/cozytest.sh
# rewrites every line matching ^}$ into `return 0` plus `}`, helpers included.
render_script() {
  out=$1
  # The chart's own default is off -- the platform turns it on per bundle -- so
  # the init container this file tests only exists when it is set here.
  helm template --set stageCniPlugins=true "$CHART" \
    | yq eval 'select(.kind == "DaemonSet") | .spec.template.spec.initContainers[] | select(.name == "install-cni-plugins") | .command[2]' - \
    | sed -e "s|/host/opt/cni/bin|$2|g" -e "s|/cni-plugins|$3|g" \
          -e "s|/dev/termination-log|${out}.termlog|g" > "$out"
  [ -s "$out" ] || return 1

  # Nothing may still point at a real /opt/cni/bin: the rebinding substitutes
  # /host/opt/cni/bin literally, and these tests EXECUTE the script.
  if grep -q '/opt/cni/bin' "$out"; then
    echo "rendered script still references a real /opt/cni/bin after rebinding;" >&2
    echo "refusing to execute it. Check that the manifest still writes through" >&2
    echo "/host/opt/cni/bin, which is what this rebinding substitutes." >&2
    return 1
  fi
}

# The names the image actually stages, read from the Dockerfile rather than
# repeated here: a script that skips a plugin by name is only exercised against
# a fixture that contains that name.
plugin_names() {
  sed -n '/tar -xz -C \/cni-plugins -f /,/;/p' \
    packages/system/multus/images/multus-cni/Dockerfile |
    tr ' ' '\n' | sed -n 's|^\./||p' | tr -d ';' | sort
}

# A fake image payload: plugin binaries that report their own name when run.
make_plugins() {
  mkdir -p "$1"
  for p in $(plugin_names); do
    printf '#!/bin/sh\necho %s-NEW\n' "$p" > "$1/$p"
    chmod 0755 "$1/$p"
  done
}

# Put an `rm` in $1 that refuses any path containing $2, leaving it in place, and
# delegates the rest. Injected rather than provoked with permissions: root's DAC
# override unlinks through a forbidding parent, so a filesystem-shaped fixture
# stops reproducing wherever the suite runs as root. $2 is narrow so staging can
# still succeed around one bad path.
stub_failing_rm() {
  mkdir -p "$1"
  sed -e "s|@MATCH@|$2|" > "$1/rm" <<'STUB'
#!/bin/sh
for a in "$@"; do
  case $a in
    *@MATCH@*)
      echo "rm: $a: Read-only file system" >&2
      exit 1
      ;;
  esac
done
exec /bin/rm "$@"
STUB
  chmod 0755 "$1/rm"
}

@test "a render that produces no script fails instead of passing as a no-op" {
  tmp=$(mktemp -d)
  mkdir -p "$tmp/dst"
  # Guards the guard. Two tests below assert nothing landed in the destination,
  # so an unrendered script satisfies them. This also fails if the check is
  # moved into last-command position, where cozytest.sh's `return 0` rewrite
  # swallows its status. Each @test runs in its own subshell, so reassigning
  # CHART cannot leak.
  CHART=$tmp/no-such-chart
  if render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src" 2>/dev/null; then
    echo "FAIL: render_script reported success with no chart to render"
    false
  fi
  rm -rf "$tmp"
}

@test "installs every staged plugin into the host cni bin dir, executable" {
  tmp=$(mktemp -d)
  make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
  render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"
  sh "$tmp/s.sh"

  names=$(plugin_names)
  [ -n "$names" ]
  [ "$(printf '%s\n' "$names" | grep -c .)" = "14" ]
  [ "$(ls -1 "$tmp/dst" | wc -l | tr -d ' ')" = "14" ]
  # Each by name: a count is satisfied by that many of anything, and one plugin
  # landing unexecutable leaves the others to carry the assertion.
  for p in $names; do
    [ -f "$tmp/dst/$p" ]
    [ -x "$tmp/dst/$p" ]
    [ "$("$tmp/dst/$p")" = "$p-NEW" ]
  done
  rm -rf "$tmp"
}

@test "installs a plugin executable even when the staged copy is not" {
  tmp=$(mktemp -d)
  make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
  # cp reproduces the source mode, so the chmod is only load-bearing when a
  # staged plugin is not already 0755.
  chmod 0644 "$tmp/src/bridge"
  render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"
  sh "$tmp/s.sh"

  # Two statements, not `[ -f X ] && [ -x X ]`: a failing command that is not the
  # last in an AND-OR list is exempt from errexit, so the pair silently continues
  # into cozytest.sh's appended `return 0` and the whole assertion is discarded.
  # Measured in dash and bash -- the `-f` half of every such pair was dead.
  [ -f "$tmp/dst/bridge" ]
  [ -x "$tmp/dst/bridge" ]
  rm -rf "$tmp"
}

@test "replaces a plugin by rename, not by writing onto the live path" {
  tmp=$(mktemp -d)
  make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
  # Stand in for a plugin another CNI already installed and the kubelet may exec.
  printf '#!/bin/sh\necho portmap-OLD\n' > "$tmp/dst/portmap"
  chmod 0755 "$tmp/dst/portmap"
  before=$(ls -i "$tmp/dst/portmap" | awk '{print $1}')

  render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"
  sh "$tmp/s.sh"

  after=$(ls -i "$tmp/dst/portmap" | awk '{print $1}')
  [ "$("$tmp/dst/portmap")" = "portmap-NEW" ]
  # A rename swaps in a new inode, so an exec holding the old one keeps a
  # complete binary; a cp onto the live path truncates and rewrites in place.
  if [ "$before" = "$after" ]; then echo "FAIL: portmap replaced in place (inode $before unchanged) — copy was not atomic"; false; fi
  rm -rf "$tmp"
}

@test "leaves no temp files behind, and clears ones stranded by an earlier kill" {
  tmp=$(mktemp -d)
  make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
  # Named for a plugin this image does not stage, so no iteration renames it
  # away -- only the cleanup can remove it.
  echo stranded > "$tmp/dst/.tmp-cozystack-multus-plugin-from-an-older-image"

  render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"
  sh "$tmp/s.sh"

  [ "$(ls -a1 "$tmp/dst" | grep -c '^\.tmp-cozystack-multus-' || true)" = "0" ]
  rm -rf "$tmp"
}

@test "skips staging instead of failing when the image predates the plugins" {
  tmp=$(mktemp -d)
  mkdir -p "$tmp/dst"
  # No source dir at all: the release-prep digest re-pin lags a Dockerfile
  # change, so the pinned image can have no /cni-plugins.
  render_script "$tmp/s.sh" "$tmp/dst" "$tmp/absent"
  sh "$tmp/s.sh" 2>"$tmp/err"

  [ "$(ls -1 "$tmp/dst" | wc -l | tr -d ' ')" = "0" ]
  # The empty destination alone is satisfied by the guard being gone: the glob
  # stays literal and `[ -e "$plugin" ]` skips it. Only this line separates them.
  grep -q 'in this image; skipping staging' "$tmp/err"
  # And nothing may be reported as failed: a skip is not a partial install.
  if grep -q 'failed to install:' "$tmp/err"; then
    echo "FAIL: an absent plugin directory was reported as failed installs"
    cat "$tmp/err"
    false
  fi
  rm -rf "$tmp"
}

@test "does not fail when the staged plugin directory is empty" {
  tmp=$(mktemp -d)
  mkdir -p "$tmp/src" "$tmp/dst"
  render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"
  # An unmatched glob stays literal in sh, so without the `[ -e ]` guard the loop
  # runs once for a path that does not exist and reports a plugin named `*`.
  sh "$tmp/s.sh" 2>"$tmp/err"

  [ "$(ls -1 "$tmp/dst" | wc -l | tr -d ' ')" = "0" ]
  # Asserted, because the empty destination alone is satisfied by the guard
  # being gone.
  if grep -q 'failed to install:' "$tmp/err"; then
    echo "FAIL: an empty plugin directory was reported as failed installs"
    cat "$tmp/err"
    false
  fi
  rm -rf "$tmp"
}

@test "a leftover directory is cleared, and the plugin installs as a file" {
  tmp=$(mktemp -d)
  make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
  # The temp path as an ordinary directory `rm -rf` can clear: a previous run
  # killed between the cp and the rename. Staging proceeds.
  #
  # The assertions check for a regular FILE: `-x` alone is true of a directory.
  mkdir -p "$tmp/dst/.tmp-cozystack-multus-bridge"
  render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"
  sh "$tmp/s.sh"

  [ -f "$tmp/dst/bridge" ]
  [ -x "$tmp/dst/bridge" ]
  [ "$("$tmp/dst/bridge")" = "bridge-NEW" ]
  rm -rf "$tmp"
}

@test "a temp path that will not clear is refused, not published as a plugin" {
  tmp=$(mktemp -d)
  make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
  # A temp path that is still a directory when the copy runs: cp writes INTO it,
  # chmod works on it, and mv publishes it as the plugin, because a rename onto
  # a free name succeeds. The runtime cannot exec a directory.
  mkdir -p "$tmp/dst/.tmp-cozystack-multus-bridge"
  stub_failing_rm "$tmp/bin" ".tmp-cozystack-multus-bridge"
  render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"

  # Still must not fail the container. hostNetwork pods bypass CNI and would
  # start either way; every other pod on the node would not.
  if ! PATH="$tmp/bin:$PATH" sh "$tmp/s.sh" 2>"$tmp/err"; then
    echo "FAIL: an unclearable temp path failed the container"
    cat "$tmp/err"
    false
  fi
  if [ -d "$tmp/dst/bridge" ]; then
    echo "FAIL: a directory was published as the plugin"
    ls -la "$tmp/dst/bridge"
    false
  fi
  # Refused, so nothing is there at all -- not a half-installed something.
  if [ -e "$tmp/dst/bridge" ]; then
    echo "FAIL: expected bridge to be absent after a refusal"
    false
  fi
  # The injection has to have happened: if PATH stopped being honoured, the real
  # `rm` would clear the fixture and every assertion above would still pass.
  if [ ! -d "$tmp/dst/.tmp-cozystack-multus-bridge" ]; then
    echo "FAIL: the temp path was removed, so no failure was injected;"
    echo "      this test proved nothing. Check that stub_failing_rm is on PATH"
    echo "      and that the script still invokes rm by bare name."
    false
  fi
  # And WHICH plugin: anchored at line start, matched as a whole token. The stub
  # prints the bridge path itself, `.*bridge` would also accept `bridgeport`,
  # and without `^` the contradictory `not failed to install:` passes too.
  grep -qE '^failed to install:( [^ ]+)* bridge( |$)' "$tmp/err"
  # The termination log is the channel the stub cannot write to, so assert there
  # too -- it is also what `kubectl describe` shows.
  grep -qE '^failed to install:( [^ ]+)* bridge( |$)' "$tmp/s.sh.termlog"
  # The other plugins are unaffected: one bad temp path is not a stop signal.
  [ -f "$tmp/dst/macvlan" ]
  [ -x "$tmp/dst/macvlan" ]
  [ "$("$tmp/dst/macvlan")" = "macvlan-NEW" ]
  rm -rf "$tmp"
}

@test "refuses to nest a plugin inside a directory that already holds its name" {
  tmp=$(mktemp -d)
  make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
  # mv moves its source INTO an existing directory, under the SOURCE's basename
  # -- the temp name, never <dst>/bridge/bridge. Assert the directory gained no
  # entries: a temp file one level down is deeper than the trap's glob.
  mkdir -p "$tmp/dst/bridge/occupied"
  render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"
  sh "$tmp/s.sh" 2>"$tmp/err"

  nested=$(ls -A "$tmp/dst/bridge" | grep -v '^occupied$' || :)
  if [ -n "$nested" ]; then
    echo "FAIL: something was moved inside the existing directory: $nested"
    false
  fi
  # One line, summary and name together: as two greps this passes when the
  # summary names a different plugin and `mv` merely printed this one.
  grep -qE '^failed to install:( [^ ]+)* bridge( |$)' "$tmp/err"
  # And a CAUSE: this is the one branch that records a failure no command
  # printed an error for, so without this line the summary is unexplained.
  grep -q 'is a directory, not a plugin binary' "$tmp/err"
  rm -rf "$tmp"
}

@test "survives a leftover it cannot clean up" {
  tmp=$(mktemp -d)
  make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
  # A leftover only the trap's glob reaches. Its `rm` is guarded because a
  # command failing inside an EXIT trap sets the status even after `exit 0`.
  mkdir -p "$tmp/dst/.tmp-cozystack-multus-stranded"
  stub_failing_rm "$tmp/bin" "stranded"
  render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"

  if ! PATH="$tmp/bin:$PATH" sh "$tmp/s.sh" 2>"$tmp/err"; then
    echo "FAIL: a leftover that could not be removed failed the container;"
    echo "      every pod that needs CNI would then fail to start here"
    cat "$tmp/err"
    false
  fi
  # The leftover has to have SURVIVED: that is the only proof the failure was
  # injected, since a real `rm -rf` would clear it and staging would succeed.
  if [ ! -d "$tmp/dst/.tmp-cozystack-multus-stranded" ]; then
    echo "FAIL: the stranded leftover was removed, so no failure was injected;"
    echo "      this test proved nothing about the EXIT trap's guard."
    false
  fi
  # And the staging still has to have happened.
  [ -f "$tmp/dst/bridge" ]
  [ -x "$tmp/dst/bridge" ]
  [ "$("$tmp/dst/bridge")" = "bridge-NEW" ]
  rm -rf "$tmp"
}

@test "reports a plugin it could not install, and still leaves the node usable" {
  tmp=$(mktemp -d)
  make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
  # An entry cp cannot copy, sorted ahead of the good plugins so a later
  # iteration still runs.
  mkdir -p "$tmp/src/aaa-uncopyable"
  # Plus a leftover the trap cannot remove, so both failure paths run at once:
  # unguarded, the script dies in the EXIT trap before the summary.
  mkdir -p "$tmp/dst/.tmp-cozystack-multus-stranded"
  stub_failing_rm "$tmp/bin" "stranded"
  render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"

  # Both halves: exit 0 alone would pass with the message dropped, and the log
  # line is the only remaining signal.
  if ! PATH="$tmp/bin:$PATH" sh "$tmp/s.sh" 2>"$tmp/err"; then
    echo "FAIL: a plugin that could not be installed failed the container;"
    echo "      every pod that needs CNI would then fail to start here"
    false
  fi
  # The script's own wording AND the name, as one line: cp prints the path it
  # could not copy, so either half alone passes with the report deleted.
  if ! grep -qE '^failed to install:( [^ ]+)* aaa-uncopyable( |$)' "$tmp/err"; then
    echo "FAIL: the script did not report the plugin it could not install"
    cat "$tmp/err"
    false
  fi

  # And into the termination log, which is what `kubectl describe` shows.
  # invisible to anyone not reading that container's logs specifically.
  if ! grep -q 'failed to install:.*aaa-uncopyable' "$tmp/s.sh.termlog" 2>/dev/null; then
    echo "FAIL: the failure was not written to the termination log"
    false
  fi

  # The stranded leftover has to have survived, or the injected failure never
  # happened and the both-go-wrong path was not exercised.
  if [ ! -d "$tmp/dst/.tmp-cozystack-multus-stranded" ]; then
    echo "FAIL: the stranded leftover was removed, so no rm failure was injected"
    false
  fi

  # The failure must not take the rest of the set with it.
  [ -f "$tmp/dst/bridge" ]
  [ -x "$tmp/dst/bridge" ]
  [ "$("$tmp/dst/bridge")" = "bridge-NEW" ]
  # And no temp FILE of its own. The planted directory is expected to survive:
  # stub_failing_rm refuses that path, which is what makes the cleanup fail.
  # Counting only regular files keeps the fixture from hiding a real leak.
  [ "$(find "$tmp/dst" -maxdepth 1 -type f -name '.tmp-cozystack-multus-*' | wc -l | tr -d ' ')" = "0" ]
  rm -rf "$tmp"
}