From 1c8c8e97744f1f7af1f062b380c1810522f55abc Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Wed, 21 Oct 2026 09:21:30 +0200 Subject: [PATCH 01/10] Moved 001_vars test into 001_basics/ Signed-off-by: Victor Moene --- tests/reactor/run.sh | 4 ++-- tests/reactor/tests/{ => 00_basics}/001_vars.cf | 2 +- tests/reactor/tests/{ => 00_basics}/001_vars.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename tests/reactor/tests/{ => 00_basics}/001_vars.cf (93%) rename tests/reactor/tests/{ => 00_basics}/001_vars.sh (93%) diff --git a/tests/reactor/run.sh b/tests/reactor/run.sh index e6acc659a3d..324a04acc61 100644 --- a/tests/reactor/run.sh +++ b/tests/reactor/run.sh @@ -3,7 +3,7 @@ fail=0 banner() { - name=" $(basename "$1") " + name=" $(basename "$(dirname "$1")")/$(basename "$1") " width=72 left=$(( (width - ${#name}) / 2 )) right=$(( width - ${#name} - left )) @@ -23,7 +23,7 @@ error() { CF_BINDIR=${CF_BINDIR:-/var/cfengine/bin} export CF_BINDIR -for t in "$PWD"/tests/*.sh; do +for t in "$PWD"/tests/*/*.sh; do banner "$t" # create throwaway work dir diff --git a/tests/reactor/tests/001_vars.cf b/tests/reactor/tests/00_basics/001_vars.cf similarity index 93% rename from tests/reactor/tests/001_vars.cf rename to tests/reactor/tests/00_basics/001_vars.cf index 43d1400ca09..25b7e09d5d3 100644 --- a/tests/reactor/tests/001_vars.cf +++ b/tests/reactor/tests/00_basics/001_vars.cf @@ -1,6 +1,6 @@ body common control { - inputs => { "../lib.cf" }; + inputs => { "../../lib.cf" }; bundlesequence => { "test" }; version => "1.0"; } diff --git a/tests/reactor/tests/001_vars.sh b/tests/reactor/tests/00_basics/001_vars.sh similarity index 93% rename from tests/reactor/tests/001_vars.sh rename to tests/reactor/tests/00_basics/001_vars.sh index 588e951218f..5fcd0838bd4 100644 --- a/tests/reactor/tests/001_vars.sh +++ b/tests/reactor/tests/00_basics/001_vars.sh @@ -2,7 +2,7 @@ set -ex -. "$(dirname "$0")/../lib.sh" +. "$(dirname "$0")/../../lib.sh" TESTFILE="$WORKDIR/tmp/test" EVENTS_FILE="$WORKDIR/tmp/events" From 59ae9328078c0406dc052869c4b75b564397d6dd Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Wed, 21 Oct 2026 14:51:05 +0200 Subject: [PATCH 02/10] Fixed Log message logging the number of watcher registered Signed-off-by: Victor Moene --- cf-reactor/watcher.c | 10 +++++++++- cf-reactor/watcher.h | 3 ++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/cf-reactor/watcher.c b/cf-reactor/watcher.c index c72bd8ffb9c..9d42156cd31 100644 --- a/cf-reactor/watcher.c +++ b/cf-reactor/watcher.c @@ -299,7 +299,9 @@ bool EventWatcherInitialize(int *fd) *fd = WakeupChannelReadFd(&wakeup_channel); - Log(LOG_LEVEL_VERBOSE, "Started cf-reactor event watcher with %zu watcher(s)", MapSize(watchers)); + /* No watchers yet, they are registered from the policy afterwards and + * counted by EventWatcherWakeUp() */ + Log(LOG_LEVEL_VERBOSE, "Started cf-reactor event watcher"); return true; } @@ -395,6 +397,12 @@ void EventWatcherResume(void) void EventWatcherWakeUp(void) { + /* Called once the watchers of the (re-)read policy are registered */ + ThreadLock(&watchers_mutex); + const size_t count = MapSize(watchers); + ThreadUnlock(&watchers_mutex); + Log(LOG_LEVEL_VERBOSE, "cf-reactor event watcher has %zu watcher(s)", count); + if (watcher_thread != NULL) { StoppableThreadWake(watcher_thread); diff --git a/cf-reactor/watcher.h b/cf-reactor/watcher.h index 60134096091..c23768a7a80 100644 --- a/cf-reactor/watcher.h +++ b/cf-reactor/watcher.h @@ -132,7 +132,8 @@ void EventWatcherResume(void); * immediately. Without watchers, the watcher thread sleeps until woken up, so * this must be called after registering watchers outside of an * EventWatcherPause()/EventWatcherResume() pair (which wakes it up itself), - * i.e. after the watchers of the initial policy. + * i.e. after the watchers of the initial policy. Logs the number of + * registered watchers. */ void EventWatcherWakeUp(void); void EventWatcherFinalize(void); From b8a1e517426578732f9cda57fcf6fe8b95df2524 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Wed, 21 Oct 2026 14:52:03 +0200 Subject: [PATCH 03/10] Fixed how the outcome of "then" bundles is recorded. The check was removed by accident in 9c10ad3fc. Now it is reverted back to how it was Signed-off-by: Victor Moene --- cf-reactor/reactor_transform.c | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/cf-reactor/reactor_transform.c b/cf-reactor/reactor_transform.c index ce03fc86f0a..d7a9cc971b9 100644 --- a/cf-reactor/reactor_transform.c +++ b/cf-reactor/reactor_transform.c @@ -505,7 +505,25 @@ static PromiseResult KeepEventsPromiseOnEvent(EvalContext *ctx, const Promise *p return PROMISE_RESULT_SKIPPED; } - return RunThenBundles(ctx, pp); + PromiseResult result; + switch (event->check) + { + case WATCHER_CHECK_EVENT: + result = RunThenBundles(ctx, pp); + break; + + case WATCHER_CHECK_ERROR: + /* Already logged as an error by the watcher, see HandleQueuedEvents() */ + result = PROMISE_RESULT_FAIL; + break; + + default: + ProgrammingError("Unexpected watcher check result %d for reactor events promise '%s'", + (int) event->check, pp->promiser); + } + + RecordEventsPromiseOutcome(ctx, pp, result); + return result; } void HandleReactorEvent(EvalContext *ctx, const Promise *pp, const char *promise_hash, WatcherCheckResult check) From 8393e20aea35c3adbf76aebbbd4c664b807b83e9 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Wed, 21 Oct 2026 14:54:28 +0200 Subject: [PATCH 04/10] Added test coverage to cf-reactor These tests cover the reactor bundles and the events promise type. Signed-off-by: Victor Moene --- tests/reactor/lib.sh | 49 ++++++++++- tests/reactor/run.sh | 48 ++++++++++- tests/reactor/tests/00_basics/001_vars.cf | 5 ++ tests/reactor/tests/00_basics/001_vars.sh | 6 +- tests/reactor/tests/00_basics/002_classes.cf | 40 +++++++++ tests/reactor/tests/00_basics/002_classes.sh | 31 +++++++ tests/reactor/tests/00_basics/003_meta.cf | 33 ++++++++ tests/reactor/tests/00_basics/003_meta.sh | 25 ++++++ .../00_basics/004_multiple_reactor_bundles.cf | 32 +++++++ .../00_basics/004_multiple_reactor_bundles.sh | 28 +++++++ .../tests/00_basics/005_multiple_events.cf | 34 ++++++++ .../tests/00_basics/005_multiple_events.sh | 42 ++++++++++ .../tests/00_basics/006_identical_events.cf | 37 +++++++++ .../tests/00_basics/006_identical_events.sh | 30 +++++++ .../tests/00_basics/007_promiser_list.cf | 30 +++++++ .../tests/00_basics/007_promiser_list.sh | 42 ++++++++++ .../tests/00_basics/008_inherit_from.cf | 34 ++++++++ .../tests/00_basics/008_inherit_from.sh | 28 +++++++ tests/reactor/tests/00_basics/009_interval.cf | 30 +++++++ tests/reactor/tests/00_basics/009_interval.sh | 37 +++++++++ .../tests/00_basics/010_then_bundle_list.cf | 33 ++++++++ .../tests/00_basics/010_then_bundle_list.sh | 34 ++++++++ .../tests/00_basics/011_then_common_bundle.cf | 48 +++++++++++ .../tests/00_basics/011_then_common_bundle.sh | 28 +++++++ .../00_basics/012_then_namespaced_bundle.cf | 57 +++++++++++++ .../00_basics/012_then_namespaced_bundle.sh | 29 +++++++ .../tests/00_basics/013_then_state_reset.cf | 60 ++++++++++++++ .../tests/00_basics/013_then_state_reset.sh | 41 +++++++++ .../tests/00_basics/014_persistent_classes.cf | 45 ++++++++++ .../tests/00_basics/014_persistent_classes.sh | 42 ++++++++++ .../00_basics/015_outcome_setup_failed.cf | 49 +++++++++++ .../00_basics/015_outcome_setup_failed.sh | 32 +++++++ .../tests/00_basics/016_outcome_setup_kept.cf | 46 ++++++++++ .../tests/00_basics/016_outcome_setup_kept.sh | 29 +++++++ .../00_basics/017_outcome_polling_failed.cf | 60 ++++++++++++++ .../00_basics/017_outcome_polling_failed.sh | 40 +++++++++ .../00_basics/018_outcome_then_bundle.cf | 83 +++++++++++++++++++ .../00_basics/018_outcome_then_bundle.sh | 42 ++++++++++ .../tests/00_basics/019_override_ifelapsed.cf | 52 ++++++++++++ .../tests/00_basics/019_override_ifelapsed.sh | 40 +++++++++ .../tests/00_basics/020_policy_reload.cf | 37 +++++++++ .../tests/00_basics/020_policy_reload.sh | 83 +++++++++++++++++++ tests/reactor/tests/00_basics/021_no_file.cf | 30 +++++++ tests/reactor/tests/00_basics/021_no_file.sh | 37 +++++++++ 44 files changed, 1711 insertions(+), 7 deletions(-) create mode 100644 tests/reactor/tests/00_basics/002_classes.cf create mode 100644 tests/reactor/tests/00_basics/002_classes.sh create mode 100644 tests/reactor/tests/00_basics/003_meta.cf create mode 100644 tests/reactor/tests/00_basics/003_meta.sh create mode 100644 tests/reactor/tests/00_basics/004_multiple_reactor_bundles.cf create mode 100644 tests/reactor/tests/00_basics/004_multiple_reactor_bundles.sh create mode 100644 tests/reactor/tests/00_basics/005_multiple_events.cf create mode 100644 tests/reactor/tests/00_basics/005_multiple_events.sh create mode 100644 tests/reactor/tests/00_basics/006_identical_events.cf create mode 100644 tests/reactor/tests/00_basics/006_identical_events.sh create mode 100644 tests/reactor/tests/00_basics/007_promiser_list.cf create mode 100644 tests/reactor/tests/00_basics/007_promiser_list.sh create mode 100644 tests/reactor/tests/00_basics/008_inherit_from.cf create mode 100644 tests/reactor/tests/00_basics/008_inherit_from.sh create mode 100644 tests/reactor/tests/00_basics/009_interval.cf create mode 100644 tests/reactor/tests/00_basics/009_interval.sh create mode 100644 tests/reactor/tests/00_basics/010_then_bundle_list.cf create mode 100644 tests/reactor/tests/00_basics/010_then_bundle_list.sh create mode 100644 tests/reactor/tests/00_basics/011_then_common_bundle.cf create mode 100644 tests/reactor/tests/00_basics/011_then_common_bundle.sh create mode 100644 tests/reactor/tests/00_basics/012_then_namespaced_bundle.cf create mode 100644 tests/reactor/tests/00_basics/012_then_namespaced_bundle.sh create mode 100644 tests/reactor/tests/00_basics/013_then_state_reset.cf create mode 100644 tests/reactor/tests/00_basics/013_then_state_reset.sh create mode 100644 tests/reactor/tests/00_basics/014_persistent_classes.cf create mode 100644 tests/reactor/tests/00_basics/014_persistent_classes.sh create mode 100644 tests/reactor/tests/00_basics/015_outcome_setup_failed.cf create mode 100644 tests/reactor/tests/00_basics/015_outcome_setup_failed.sh create mode 100644 tests/reactor/tests/00_basics/016_outcome_setup_kept.cf create mode 100644 tests/reactor/tests/00_basics/016_outcome_setup_kept.sh create mode 100644 tests/reactor/tests/00_basics/017_outcome_polling_failed.cf create mode 100644 tests/reactor/tests/00_basics/017_outcome_polling_failed.sh create mode 100644 tests/reactor/tests/00_basics/018_outcome_then_bundle.cf create mode 100644 tests/reactor/tests/00_basics/018_outcome_then_bundle.sh create mode 100644 tests/reactor/tests/00_basics/019_override_ifelapsed.cf create mode 100644 tests/reactor/tests/00_basics/019_override_ifelapsed.sh create mode 100644 tests/reactor/tests/00_basics/020_policy_reload.cf create mode 100644 tests/reactor/tests/00_basics/020_policy_reload.sh create mode 100644 tests/reactor/tests/00_basics/021_no_file.cf create mode 100644 tests/reactor/tests/00_basics/021_no_file.sh diff --git a/tests/reactor/lib.sh b/tests/reactor/lib.sh index 6b24ef43038..f49a25905a2 100644 --- a/tests/reactor/lib.sh +++ b/tests/reactor/lib.sh @@ -1,4 +1,4 @@ -# Helpers sourced by the tests. Expects CF_BINDIR and LOG to be exported by +# Helpers sourced by the tests. Expects CF_REACTOR and LOG to be exported by # run.sh. # number of log lines already consumed by wait_for_log @@ -19,6 +19,21 @@ wait_for_log() { log_offset=$((log_offset + ${match%%:*})) } +# Wait until cf-reactor has registered the watchers of the policy, $1 of them. +# The watcher thread may log before this line, so search the whole log and +# leave the offset of wait_for_log as is. +wait_for_watchers() { + if ! timeout 60 sh -c ' + until grep -q -F -- "$2" "$1"; do + sleep 0.1 + done' _ "$LOG" "verbose: cf-reactor event watcher has $1 watcher(s)" + then + echo "Timed out waiting for $1 watcher(s) in reactor log:" >&2 + cat "$LOG" >&2 + return 1 + fi +} + # Wait until file $1 has at least $2 lines, then fail if it has more than $2. wait_for_lines() { if ! timeout 60 sh -c ' @@ -39,10 +54,40 @@ wait_for_lines() { fi } +# Fail unless line $2 of file $1, without the timestamp written by +# record_event, is $3. +check_event() { + line=$(sed -n "$2p" "$1" | cut -d' ' -f3-) + if [ "$line" != "$3" ] + then + echo "Expected line $2 of '$1' to be '$3', got '$line':" >&2 + cat "$1" >&2 + return 1 + fi +} + +# Fail unless lines $2 to $3 of file $1, without the timestamps written by +# record_event, are the remaining arguments in any order, for events whose +# order is not defined. +check_events_unordered() { + file=$1 first=$2 last=$3 + shift 3 + actual=$(sed -n "${first},${last}p" "$file" | cut -d' ' -f3- | sort) + expected=$(printf '%s\n' "$@" | sort) + if [ "$actual" != "$expected" ] + then + echo "Expected lines $first to $last of '$file' to be, in any order:" >&2 + printf '%s\n' "$@" >&2 + echo "got:" >&2 + cat "$file" >&2 + return 1 + fi +} + # Start cf-reactor in the background with policy file $1, logging to $LOG. # It is stopped when the test exits. start_reactor() { - "$CF_BINDIR/cf-reactor" --no-fork --verbose -f "$1" >>"$LOG" 2>&1 & + "$CF_REACTOR" --no-fork --verbose -f "$1" >>"$LOG" 2>&1 & reactor_pid=$! trap stop_reactor EXIT } diff --git a/tests/reactor/run.sh b/tests/reactor/run.sh index 324a04acc61..9612c34fcf1 100644 --- a/tests/reactor/run.sh +++ b/tests/reactor/run.sh @@ -1,5 +1,13 @@ #!/bin/sh +# Usage: run.sh +# +# Must be run from the tests/reactor directory. +# +# The binaries are taken from $CF_BINDIR if it is set, otherwise from the +# CFEngine build tree this directory is part of if they are built, otherwise +# from /var/cfengine/bin. + fail=0 banner() { @@ -20,8 +28,29 @@ error() { fail=1 } -CF_BINDIR=${CF_BINDIR:-/var/cfengine/bin} -export CF_BINDIR +# Print the path of binary $1, see the usage above. +find_binary() { + if [ -n "$CF_BINDIR" ]; then + echo "$CF_BINDIR/$1" + elif [ -x "$PWD/../../$1/$1" ]; then + # in-tree build, may be a libtool wrapper script + echo "$PWD/../../$1/$1" + else + echo "/var/cfengine/bin/$1" + fi +} + +CF_REACTOR=$(find_binary cf-reactor) +CF_PROMISES=$(find_binary cf-promises) +export CF_REACTOR + +for b in "$CF_REACTOR" "$CF_PROMISES"; do + if [ ! -x "$b" ]; then + echo "'$b' not found, build CFEngine or set CF_BINDIR" >&2 + exit 2 + fi +done +echo "Using $CF_REACTOR and $CF_PROMISES" for t in "$PWD"/tests/*/*.sh; do banner "$t" @@ -29,13 +58,24 @@ for t in "$PWD"/tests/*/*.sh; do # create throwaway work dir WORKDIR=$(mktemp -d) mkdir -p "$WORKDIR/bin" "$WORKDIR/tmp" - ln -s "$CF_BINDIR/cf-promises" "$WORKDIR/bin/" + ln -s "$CF_PROMISES" "$WORKDIR/bin/cf-promises" + + # Run a copy of the policy, keeping its path relative to lib.cf, as + # CFEngine refuses to read policy files writable by group or others, which + # those of a checkout made with a umask of 002 are + rel=${t#"$PWD"/} + policy="$WORKDIR/policy/${rel%.sh}.cf" + mkdir -p "$(dirname "$policy")" + cp lib.cf "$WORKDIR/policy/" + cp "${t%.sh}.cf" "$policy" + chmod 600 "$WORKDIR/policy/lib.cf" "$policy" + CFENGINE_TEST_OVERRIDE_WORKDIR="$WORKDIR" LOG="$WORKDIR/reactor.log" PROMISE_NAME=$(basename "$t" .sh) export WORKDIR CFENGINE_TEST_OVERRIDE_WORKDIR LOG PROMISE_NAME - sh "$t" "${t%.sh}.cf" || error "$t" + sh "$t" "$policy" || error "$t" rm -rf "$WORKDIR" done diff --git a/tests/reactor/tests/00_basics/001_vars.cf b/tests/reactor/tests/00_basics/001_vars.cf index 25b7e09d5d3..e585cb84d03 100644 --- a/tests/reactor/tests/00_basics/001_vars.cf +++ b/tests/reactor/tests/00_basics/001_vars.cf @@ -1,3 +1,8 @@ +####################################################### +# +# Test vars promises in reactor bundle +# +####################################################### body common control { inputs => { "../../lib.cf" }; diff --git a/tests/reactor/tests/00_basics/001_vars.sh b/tests/reactor/tests/00_basics/001_vars.sh index 5fcd0838bd4..fb4ef67dabd 100644 --- a/tests/reactor/tests/00_basics/001_vars.sh +++ b/tests/reactor/tests/00_basics/001_vars.sh @@ -4,6 +4,8 @@ set -ex . "$(dirname "$0")/../../lib.sh" +POLICY_FILE="$1" + TESTFILE="$WORKDIR/tmp/test" EVENTS_FILE="$WORKDIR/tmp/events" export TESTFILE EVENTS_FILE @@ -11,14 +13,16 @@ export TESTFILE EVENTS_FILE # create initial file touch "$TESTFILE" -start_reactor "$1" +start_reactor "$POLICY_FILE" wait_for_log "File '$TESTFILE' is present, watching for its deletion" rm "$TESTFILE" wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "$TESTFILE was deleted" # recreate the file and wait for the watcher to see it before deleting it again touch "$TESTFILE" wait_for_log "File '$TESTFILE' is present, watching for its deletion" rm "$TESTFILE" wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "$TESTFILE was deleted" diff --git a/tests/reactor/tests/00_basics/002_classes.cf b/tests/reactor/tests/00_basics/002_classes.cf new file mode 100644 index 00000000000..322b1ed3418 --- /dev/null +++ b/tests/reactor/tests/00_basics/002_classes.cf @@ -0,0 +1,40 @@ +####################################################### +# +# Test classes promises in reactor bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + classes: + "reactor_test_defined_class" expression => "any"; + + events: + reactor_test_defined_class:: + "$(promise_name)" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"); + + reactor_test_undefined_class:: + "$(promise_name)_2" + when => deleted("$(test)"), + then => record_event( + "$(events_file)", "$(test) was deleted (undefined class)" + ); +} diff --git a/tests/reactor/tests/00_basics/002_classes.sh b/tests/reactor/tests/00_basics/002_classes.sh new file mode 100644 index 00000000000..f5145f8eabc --- /dev/null +++ b/tests/reactor/tests/00_basics/002_classes.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +# only the promise guarded by the defined class has a watcher +wait_for_watchers 1 + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "$TESTFILE was deleted" + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "$TESTFILE was deleted" diff --git a/tests/reactor/tests/00_basics/003_meta.cf b/tests/reactor/tests/00_basics/003_meta.cf new file mode 100644 index 00000000000..0d8d966382c --- /dev/null +++ b/tests/reactor/tests/00_basics/003_meta.cf @@ -0,0 +1,33 @@ +####################################################### +# +# Test meta promises in reactor bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + meta: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + # only the meta variables, in the test_meta scope, so that this would + # fail if meta promises were evaluated like vars promises + "$(test_meta.promise_name)" + when => deleted("$(test_meta.test)"), + then => record_event( + "$(test_meta.events_file)", "$(test_meta.test) was deleted" + ); +} diff --git a/tests/reactor/tests/00_basics/003_meta.sh b/tests/reactor/tests/00_basics/003_meta.sh new file mode 100644 index 00000000000..0c53ad07234 --- /dev/null +++ b/tests/reactor/tests/00_basics/003_meta.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 + +# the meta variables also expanded in the arguments of the "then" bundle +check_event "$EVENTS_FILE" 1 "$TESTFILE was deleted" diff --git a/tests/reactor/tests/00_basics/004_multiple_reactor_bundles.cf b/tests/reactor/tests/00_basics/004_multiple_reactor_bundles.cf new file mode 100644 index 00000000000..7e2fef79e9e --- /dev/null +++ b/tests/reactor/tests/00_basics/004_multiple_reactor_bundles.cf @@ -0,0 +1,32 @@ +####################################################### +# +# Test that every reactor bundle is evaluated +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "init", "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor init +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); +} + +bundle reactor test +{ + events: + "$(init.promise_name)" + when => deleted("$(init.test)"), + then => record_event("$(init.events_file)", "$(init.test) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/004_multiple_reactor_bundles.sh b/tests/reactor/tests/00_basics/004_multiple_reactor_bundles.sh new file mode 100644 index 00000000000..fb4ef67dabd --- /dev/null +++ b/tests/reactor/tests/00_basics/004_multiple_reactor_bundles.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "$TESTFILE was deleted" + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "$TESTFILE was deleted" diff --git a/tests/reactor/tests/00_basics/005_multiple_events.cf b/tests/reactor/tests/00_basics/005_multiple_events.cf new file mode 100644 index 00000000000..39dc6fefabc --- /dev/null +++ b/tests/reactor/tests/00_basics/005_multiple_events.cf @@ -0,0 +1,34 @@ +####################################################### +# +# Test several events promise in one reactor bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "test_A" string => getenv("TESTFILE_A", 999); + "test_B" string => getenv("TESTFILE_B", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)_A" + when => deleted("$(test_A)"), + then => record_event("$(events_file)", "$(test_A) was deleted"); + + "$(promise_name)_B" + when => deleted("$(test_B)"), + then => record_event("$(events_file)", "$(test_B) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/005_multiple_events.sh b/tests/reactor/tests/00_basics/005_multiple_events.sh new file mode 100644 index 00000000000..3d9887a6c36 --- /dev/null +++ b/tests/reactor/tests/00_basics/005_multiple_events.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE_A="$WORKDIR/tmp/test_A" +TESTFILE_B="$WORKDIR/tmp/test_B" + +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE_A TESTFILE_B EVENTS_FILE + +# create initial file +touch "$TESTFILE_A" +touch "$TESTFILE_B" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 2 + +# checked in the order of the promises +wait_for_log "File '$TESTFILE_A' is present, watching for its deletion" +wait_for_log "File '$TESTFILE_B' is present, watching for its deletion" +rm "$TESTFILE_A" +rm "$TESTFILE_B" +wait_for_lines "$EVENTS_FILE" 2 +check_events_unordered "$EVENTS_FILE" 1 2 \ + "$TESTFILE_A was deleted" "$TESTFILE_B was deleted" + +# recreate the files and wait for the watchers to see them before deleting +# them again +touch "$TESTFILE_A" +touch "$TESTFILE_B" +wait_for_log "File '$TESTFILE_A' is present, watching for its deletion" +wait_for_log "File '$TESTFILE_B' is present, watching for its deletion" +rm "$TESTFILE_A" +rm "$TESTFILE_B" +wait_for_lines "$EVENTS_FILE" 4 +check_events_unordered "$EVENTS_FILE" 3 4 \ + "$TESTFILE_A was deleted" "$TESTFILE_B was deleted" diff --git a/tests/reactor/tests/00_basics/006_identical_events.cf b/tests/reactor/tests/00_basics/006_identical_events.cf new file mode 100644 index 00000000000..f2fdcbcc9fa --- /dev/null +++ b/tests/reactor/tests/00_basics/006_identical_events.cf @@ -0,0 +1,37 @@ +####################################################### +# +# Test duplicate event promises +# +# The two promises are identical, so cf-reactor only registers one watcher +# for them and the "then" bundle is run only once per event +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/006_identical_events.sh b/tests/reactor/tests/00_basics/006_identical_events.sh new file mode 100644 index 00000000000..46de0249a07 --- /dev/null +++ b/tests/reactor/tests/00_basics/006_identical_events.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "$TESTFILE was deleted" + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "$TESTFILE was deleted" diff --git a/tests/reactor/tests/00_basics/007_promiser_list.cf b/tests/reactor/tests/00_basics/007_promiser_list.cf new file mode 100644 index 00000000000..f7b5d3e1791 --- /dev/null +++ b/tests/reactor/tests/00_basics/007_promiser_list.cf @@ -0,0 +1,30 @@ +####################################################### +# +# Test promiser list expansion +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "suffix" slist => { "A", "B" }; + "test_$(suffix)" string => getenv("TESTFILE_$(suffix)", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)_$(suffix)" + when => deleted("$(test_$(suffix))"), + then => record_event("$(events_file)", "$(test_$(suffix)) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/007_promiser_list.sh b/tests/reactor/tests/00_basics/007_promiser_list.sh new file mode 100644 index 00000000000..3d9887a6c36 --- /dev/null +++ b/tests/reactor/tests/00_basics/007_promiser_list.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE_A="$WORKDIR/tmp/test_A" +TESTFILE_B="$WORKDIR/tmp/test_B" + +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE_A TESTFILE_B EVENTS_FILE + +# create initial file +touch "$TESTFILE_A" +touch "$TESTFILE_B" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 2 + +# checked in the order of the promises +wait_for_log "File '$TESTFILE_A' is present, watching for its deletion" +wait_for_log "File '$TESTFILE_B' is present, watching for its deletion" +rm "$TESTFILE_A" +rm "$TESTFILE_B" +wait_for_lines "$EVENTS_FILE" 2 +check_events_unordered "$EVENTS_FILE" 1 2 \ + "$TESTFILE_A was deleted" "$TESTFILE_B was deleted" + +# recreate the files and wait for the watchers to see them before deleting +# them again +touch "$TESTFILE_A" +touch "$TESTFILE_B" +wait_for_log "File '$TESTFILE_A' is present, watching for its deletion" +wait_for_log "File '$TESTFILE_B' is present, watching for its deletion" +rm "$TESTFILE_A" +rm "$TESTFILE_B" +wait_for_lines "$EVENTS_FILE" 4 +check_events_unordered "$EVENTS_FILE" 3 4 \ + "$TESTFILE_A was deleted" "$TESTFILE_B was deleted" diff --git a/tests/reactor/tests/00_basics/008_inherit_from.cf b/tests/reactor/tests/00_basics/008_inherit_from.cf new file mode 100644 index 00000000000..663d398bd08 --- /dev/null +++ b/tests/reactor/tests/00_basics/008_inherit_from.cf @@ -0,0 +1,34 @@ +####################################################### +# +# Test inherit_from +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when parent(file) +{ + file_deleted => "$(file)"; +} + +body when deleted(file) +{ + inherit_from => parent("$(file)"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/008_inherit_from.sh b/tests/reactor/tests/00_basics/008_inherit_from.sh new file mode 100644 index 00000000000..fb4ef67dabd --- /dev/null +++ b/tests/reactor/tests/00_basics/008_inherit_from.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "$TESTFILE was deleted" + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "$TESTFILE was deleted" diff --git a/tests/reactor/tests/00_basics/009_interval.cf b/tests/reactor/tests/00_basics/009_interval.cf new file mode 100644 index 00000000000..008cf55c893 --- /dev/null +++ b/tests/reactor/tests/00_basics/009_interval.cf @@ -0,0 +1,30 @@ +####################################################### +# +# Test interval +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; + interval => 10; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/009_interval.sh b/tests/reactor/tests/00_basics/009_interval.sh new file mode 100644 index 00000000000..4a749849370 --- /dev/null +++ b/tests/reactor/tests/00_basics/009_interval.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "$TESTFILE was deleted" +t1=$(date +%s) + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "$TESTFILE was deleted" +t2=$(date +%s) + +# checking that the interval is at minimum 10 seconds +delta=$((t2 - t1)) +if [ "$delta" -lt 10 ]; then + echo "FAIL: expected >= 10s, got ${delta}s" >&2 + exit 1 +fi diff --git a/tests/reactor/tests/00_basics/010_then_bundle_list.cf b/tests/reactor/tests/00_basics/010_then_bundle_list.cf new file mode 100644 index 00000000000..93ba04c0af4 --- /dev/null +++ b/tests/reactor/tests/00_basics/010_then_bundle_list.cf @@ -0,0 +1,33 @@ +####################################################### +# +# Test an array of bundles to run on event +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + record_event("$(events_file)", "$(test) first"), + record_event("$(events_file)", "$(test) second"), + record_event("$(events_file)", "$(test) third"), + }; +} diff --git a/tests/reactor/tests/00_basics/010_then_bundle_list.sh b/tests/reactor/tests/00_basics/010_then_bundle_list.sh new file mode 100644 index 00000000000..74c93ed05bc --- /dev/null +++ b/tests/reactor/tests/00_basics/010_then_bundle_list.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 3 + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 6 + +# the bundles are run in the order of the list +check_event "$EVENTS_FILE" 1 "$TESTFILE first" +check_event "$EVENTS_FILE" 2 "$TESTFILE second" +check_event "$EVENTS_FILE" 3 "$TESTFILE third" +check_event "$EVENTS_FILE" 4 "$TESTFILE first" +check_event "$EVENTS_FILE" 5 "$TESTFILE second" +check_event "$EVENTS_FILE" 6 "$TESTFILE third" diff --git a/tests/reactor/tests/00_basics/011_then_common_bundle.cf b/tests/reactor/tests/00_basics/011_then_common_bundle.cf new file mode 100644 index 00000000000..54ea8c624d7 --- /dev/null +++ b/tests/reactor/tests/00_basics/011_then_common_bundle.cf @@ -0,0 +1,48 @@ +####################################################### +# +# Test common bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle common setup(f, m) +{ + vars: + "file" string => "$(f)"; + "msg" string => "$(m)"; + + classes: + "should_run" expression => "true"; +} + +bundle agent run_if(b) +{ + methods: + should_run:: + "any" usebundle => $(b)("$(setup.file)", "$(setup.msg)"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + setup("$(events_file)", "$(test) was deleted"), run_if("record_event") + }; +} diff --git a/tests/reactor/tests/00_basics/011_then_common_bundle.sh b/tests/reactor/tests/00_basics/011_then_common_bundle.sh new file mode 100644 index 00000000000..fb4ef67dabd --- /dev/null +++ b/tests/reactor/tests/00_basics/011_then_common_bundle.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "$TESTFILE was deleted" + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "$TESTFILE was deleted" diff --git a/tests/reactor/tests/00_basics/012_then_namespaced_bundle.cf b/tests/reactor/tests/00_basics/012_then_namespaced_bundle.cf new file mode 100644 index 00000000000..e26cc2e5502 --- /dev/null +++ b/tests/reactor/tests/00_basics/012_then_namespaced_bundle.cf @@ -0,0 +1,57 @@ +####################################################### +# +# Test bundles in another namespace in "then" +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + "test_ns:record_without_args", + test_ns:record_with_args( + "$(events_file)", "test_ns:record_with_args ran" + ), + }; +} + +body file control +{ + namespace => "test_ns"; +} + +bundle agent record_without_args +{ + vars: + "events_file" string => getenv("EVENTS_FILE", 999); + + methods: + "any" + usebundle => default:record_event( + "$(events_file)", "test_ns:record_without_args ran" + ); +} + +bundle agent record_with_args(file, msg) +{ + methods: + "any" usebundle => default:record_event("$(file)", "$(msg)"); +} diff --git a/tests/reactor/tests/00_basics/012_then_namespaced_bundle.sh b/tests/reactor/tests/00_basics/012_then_namespaced_bundle.sh new file mode 100644 index 00000000000..280bcc0f9ce --- /dev/null +++ b/tests/reactor/tests/00_basics/012_then_namespaced_bundle.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# check that a bundle in another namespace can be run in "then", by its name +# (then => "ns:my_bundle") and called with arguments (then => ns:my_bundle(...)) + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 + +# both bundles ran, in the order of the "then" list +check_event "$EVENTS_FILE" 1 "test_ns:record_without_args ran" +check_event "$EVENTS_FILE" 2 "test_ns:record_with_args ran" diff --git a/tests/reactor/tests/00_basics/013_then_state_reset.cf b/tests/reactor/tests/00_basics/013_then_state_reset.cf new file mode 100644 index 00000000000..cbea0456c8d --- /dev/null +++ b/tests/reactor/tests/00_basics/013_then_state_reset.cf @@ -0,0 +1,60 @@ +####################################################### +# +# Test that classes and vars are reset between "then" bundle runs +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_A(b, file, msg, marker) +{ + classes: + "marker" expression => fileexists("$(marker)"); + + methods: + !marker:: + "any" usebundle => $(b)("$(file)", "$(msg)"); +} + +bundle agent run_B(b, file, value_file) +{ + vars: + # changed by the test between the events + "value" string => readfile("$(value_file)"); + + methods: + "any" usebundle => $(b)("$(file)", "$(value)"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file_A" string => getenv("EVENTS_FILE_A", 999); + "events_file_B" string => getenv("EVENTS_FILE_B", 999); + "marker" string => getenv("MARKER_FILE", 999); + "value_file" string => getenv("VALUE_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + run_A( + "record_event", + "$(events_file_A)", + "$(test) was deleted", + "$(marker)" + ), + run_B("record_event", "$(events_file_B)", "$(value_file)"), + }; +} diff --git a/tests/reactor/tests/00_basics/013_then_state_reset.sh b/tests/reactor/tests/00_basics/013_then_state_reset.sh new file mode 100644 index 00000000000..7fba3dec9c9 --- /dev/null +++ b/tests/reactor/tests/00_basics/013_then_state_reset.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE_A="$WORKDIR/tmp/events_A" +EVENTS_FILE_B="$WORKDIR/tmp/events_B" +MARKER_FILE="$WORKDIR/tmp/marker" +VALUE_FILE="$WORKDIR/tmp/value" +export TESTFILE EVENTS_FILE_A EVENTS_FILE_B MARKER_FILE VALUE_FILE + +# create initial file +touch "$TESTFILE" +printf 'first' > "$VALUE_FILE" + +start_reactor "$POLICY_FILE" + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE_A" 1 +check_event "$EVENTS_FILE_A" 1 "$TESTFILE was deleted" +wait_for_lines "$EVENTS_FILE_B" 1 +check_event "$EVENTS_FILE_B" 1 "first" + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +touch "$MARKER_FILE" +printf 'second' > "$VALUE_FILE" + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +# run_B, which runs after run_A, recorded the value read on this event, not +# the one of the previous event +wait_for_lines "$EVENTS_FILE_B" 2 +check_event "$EVENTS_FILE_B" 2 "second" +# so run_A has run too, and didn't record the event since MARKER_FILE exists +wait_for_lines "$EVENTS_FILE_A" 1 diff --git a/tests/reactor/tests/00_basics/014_persistent_classes.cf b/tests/reactor/tests/00_basics/014_persistent_classes.cf new file mode 100644 index 00000000000..3100ec98e92 --- /dev/null +++ b/tests/reactor/tests/00_basics/014_persistent_classes.cf @@ -0,0 +1,45 @@ +####################################################### +# +# Test that persistent classes remain defined between "then" bundle runs +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +body classes if_ok(class) +{ + promise_kept => { "$(class)" }; + promise_repaired => { "$(class)" }; + persist_time => "1"; +} + +bundle agent run_if(b, file, msg) +{ + methods: + !has_run:: + "any" + usebundle => $(b)("$(file)", "$(msg)"), + classes => if_ok("has_run"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => run_if("record_event", "$(events_file)", "$(test) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/014_persistent_classes.sh b/tests/reactor/tests/00_basics/014_persistent_classes.sh new file mode 100644 index 00000000000..db7ea031995 --- /dev/null +++ b/tests/reactor/tests/00_basics/014_persistent_classes.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +# check that persistent class stay alive between "then" bundle runs + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "$TESTFILE was deleted" + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" + +# wait for the "then" bundle to have run, it doesn't record the event as the +# persistent class has_run is still defined +wait_for_log "B: END bundle run_if" +wait_for_lines "$EVENTS_FILE" 1 + +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" + +# sleep for the duration of the persistent class +sleep 60 +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "$TESTFILE was deleted" diff --git a/tests/reactor/tests/00_basics/015_outcome_setup_failed.cf b/tests/reactor/tests/00_basics/015_outcome_setup_failed.cf new file mode 100644 index 00000000000..3c422043f28 --- /dev/null +++ b/tests/reactor/tests/00_basics/015_outcome_setup_failed.cf @@ -0,0 +1,49 @@ +####################################################### +# +# Test that an events promise is not kept when its watcher can't be set up +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +body classes outcome(prefix) +{ + promise_kept => { "$(prefix)_kept" }; + promise_repaired => { "$(prefix)_repaired" }; + repair_failed => { "$(prefix)_failed" }; + repair_denied => { "$(prefix)_failed" }; + repair_timeout => { "$(prefix)_failed" }; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "check" string => getenv("CHECK_FILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + + # only known when running, so cf-promises can't reject it + "missing_bundle" string => getenv("MISSING_BUNDLE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)_broken" + when => deleted("$(test)"), + then => { "$(missing_bundle)" }, + classes => outcome("broken"); + + # only set up if the outcome of the broken promise is as expected + broken_failed.!broken_kept.!broken_repaired:: + "$(promise_name)_check" + when => deleted("$(check)"), + then => record_event("$(events_file)", "$(check) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/015_outcome_setup_failed.sh b/tests/reactor/tests/00_basics/015_outcome_setup_failed.sh new file mode 100644 index 00000000000..10b1f8584b0 --- /dev/null +++ b/tests/reactor/tests/00_basics/015_outcome_setup_failed.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +# events promise is NOT KEPT when couldn't set up watchers + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +CHECK_FILE="$WORKDIR/tmp/check" +EVENTS_FILE="$WORKDIR/tmp/events" +MISSING_BUNDLE="no_such_bundle" +export TESTFILE CHECK_FILE EVENTS_FILE MISSING_BUNDLE + +# create initial files +touch "$TESTFILE" +touch "$CHECK_FILE" + +start_reactor "$POLICY_FILE" + +wait_for_log "refers to unknown bundle '$MISSING_BUNDLE'" + +# only the watcher of the check promise, which is only set up if the broken +# promise was not kept +wait_for_watchers 1 + +wait_for_log "File '$CHECK_FILE' is present, watching for its deletion" +rm "$CHECK_FILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "$CHECK_FILE was deleted" diff --git a/tests/reactor/tests/00_basics/016_outcome_setup_kept.cf b/tests/reactor/tests/00_basics/016_outcome_setup_kept.cf new file mode 100644 index 00000000000..f6905df06e7 --- /dev/null +++ b/tests/reactor/tests/00_basics/016_outcome_setup_kept.cf @@ -0,0 +1,46 @@ +####################################################### +# +# Test that an events promise is kept when its watcher is set up +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +body classes outcome(prefix) +{ + promise_kept => { "$(prefix)_kept" }; + promise_repaired => { "$(prefix)_repaired" }; + repair_failed => { "$(prefix)_failed" }; + repair_denied => { "$(prefix)_failed" }; + repair_timeout => { "$(prefix)_failed" }; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "check" string => getenv("CHECK_FILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)_ok" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"), + classes => outcome("ok"); + + # only set up if the outcome of the ok promise is as expected + ok_kept.!ok_repaired.!ok_failed:: + "$(promise_name)_check" + when => deleted("$(check)"), + then => record_event("$(events_file)", "$(check) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/016_outcome_setup_kept.sh b/tests/reactor/tests/00_basics/016_outcome_setup_kept.sh new file mode 100644 index 00000000000..94c0da24b0f --- /dev/null +++ b/tests/reactor/tests/00_basics/016_outcome_setup_kept.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# when could set up watchers, promise is kept + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +CHECK_FILE="$WORKDIR/tmp/check" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE CHECK_FILE EVENTS_FILE + +# create initial files +touch "$TESTFILE" +touch "$CHECK_FILE" + +start_reactor "$POLICY_FILE" + +# the watcher of the ok promise, and the one of the check promise, which is +# only set up if the ok promise was kept +wait_for_watchers 2 + +wait_for_log "File '$CHECK_FILE' is present, watching for its deletion" +rm "$CHECK_FILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "$CHECK_FILE was deleted" diff --git a/tests/reactor/tests/00_basics/017_outcome_polling_failed.cf b/tests/reactor/tests/00_basics/017_outcome_polling_failed.cf new file mode 100644 index 00000000000..c3581a742cb --- /dev/null +++ b/tests/reactor/tests/00_basics/017_outcome_polling_failed.cf @@ -0,0 +1,60 @@ +####################################################### +# +# Test that an events promise is not kept when its watcher fails to check +# for its event, and that its "then" bundles are not run +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +# The classes defined on an event are undone once it is handled, except the +# persistent ones, which the next events see +body classes persistent_outcome(prefix) +{ + promise_kept => { "$(prefix)_kept" }; + promise_repaired => { "$(prefix)_repaired" }; + repair_failed => { "$(prefix)_failed" }; + repair_denied => { "$(prefix)_failed" }; + repair_timeout => { "$(prefix)_failed" }; + persist_time => "10"; +} + +bundle agent record_outcome(file, prefix) +{ + methods: + "$(prefix)_failed" + if => "$(prefix)_failed", + usebundle => record_event("$(file)", "$(prefix)_failed"); + + "not $(prefix)_failed" + unless => "$(prefix)_failed", + usebundle => record_event("$(file)", "not $(prefix)_failed"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "check" string => getenv("CHECK_FILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)_failing" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"), + classes => persistent_outcome("failing"); + + "$(promise_name)_check" + when => deleted("$(check)"), + then => record_outcome("$(events_file)", "failing"); +} diff --git a/tests/reactor/tests/00_basics/017_outcome_polling_failed.sh b/tests/reactor/tests/00_basics/017_outcome_polling_failed.sh new file mode 100644 index 00000000000..a156dd503cc --- /dev/null +++ b/tests/reactor/tests/00_basics/017_outcome_polling_failed.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +# when could set up watchers, but the polling thread is failing, return NOT KEPT + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTDIR="$WORKDIR/tmp/dir" +TESTFILE="$TESTDIR/test" +CHECK_FILE="$WORKDIR/tmp/check" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE CHECK_FILE EVENTS_FILE + +# create initial files +mkdir "$TESTDIR" +touch "$TESTFILE" +touch "$CHECK_FILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 2 + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +wait_for_log "File '$CHECK_FILE' is present, watching for its deletion" + +# replace the directory of the watched file with a symlink to itself, so that +# checking the file fails (ELOOP, even as root) rather than seeing it deleted +rm -r "$TESTDIR" +ln -s "$TESTDIR" "$TESTDIR" +wait_for_log "Reactor watcher '${PROMISE_NAME}_failing' failed to check for its event" + +# the outcome of the failing promise is only visible to the next event +rm "$CHECK_FILE" +wait_for_lines "$EVENTS_FILE" 1 + +# the "then" bundle of the failing promise didn't run, and it wasn't kept +check_event "$EVENTS_FILE" 1 "failing_failed" diff --git a/tests/reactor/tests/00_basics/018_outcome_then_bundle.cf b/tests/reactor/tests/00_basics/018_outcome_then_bundle.cf new file mode 100644 index 00000000000..dd8f6c602ce --- /dev/null +++ b/tests/reactor/tests/00_basics/018_outcome_then_bundle.cf @@ -0,0 +1,83 @@ +####################################################### +# +# Test that the outcome of an events promise on its event is the outcome of +# its "then" bundles +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +# The classes defined on an event are undone once it is handled, except the +# persistent ones, which the next events see +body classes persistent_outcome(prefix) +{ + promise_repaired => { "$(prefix)_repaired" }; + repair_failed => { "$(prefix)_failed" }; + repair_denied => { "$(prefix)_failed" }; + repair_timeout => { "$(prefix)_failed" }; + persist_time => "10"; +} + +bundle agent fail +{ + commands: + "/bin/false"; +} + +bundle agent repair(file) +{ + files: + "$(file)" create => "true"; +} + +bundle agent record_outcomes(file) +{ + vars: + "classes" + slist => { + "failing_failed", + "failing_repaired", + "repairing_failed", + "repairing_repaired", + }; + + methods: + "$(classes)" + if => "$(classes)", + usebundle => record_event("$(file)", "$(classes)"); +} + +bundle reactor test +{ + vars: + "test_failing" string => getenv("TESTFILE_FAILING", 999); + "test_repairing" string => getenv("TESTFILE_REPAIRING", 999); + "created" string => getenv("CREATED_FILE", 999); + "check" string => getenv("CHECK_FILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)_failing" + when => deleted("$(test_failing)"), + then => fail, + classes => persistent_outcome("failing"); + + "$(promise_name)_repairing" + when => deleted("$(test_repairing)"), + then => repair("$(created)"), + classes => persistent_outcome("repairing"); + + "$(promise_name)_check" + when => deleted("$(check)"), + then => record_outcomes("$(events_file)"); +} diff --git a/tests/reactor/tests/00_basics/018_outcome_then_bundle.sh b/tests/reactor/tests/00_basics/018_outcome_then_bundle.sh new file mode 100644 index 00000000000..c6c6637abb9 --- /dev/null +++ b/tests/reactor/tests/00_basics/018_outcome_then_bundle.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +# if set up and polls correctly, the outcome should be the outcome of the run bundle in "then" + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE_FAILING="$WORKDIR/tmp/test_failing" +TESTFILE_REPAIRING="$WORKDIR/tmp/test_repairing" +CREATED_FILE="$WORKDIR/tmp/created" +CHECK_FILE="$WORKDIR/tmp/check" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE_FAILING TESTFILE_REPAIRING CREATED_FILE CHECK_FILE EVENTS_FILE + +# create initial files +touch "$TESTFILE_FAILING" +touch "$TESTFILE_REPAIRING" +touch "$CHECK_FILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 3 + +wait_for_log "File '$TESTFILE_FAILING' is present, watching for its deletion" +wait_for_log "File '$TESTFILE_REPAIRING' is present, watching for its deletion" +wait_for_log "File '$CHECK_FILE' is present, watching for its deletion" + +# one event at a time, the events are handled in order +rm "$TESTFILE_FAILING" +wait_for_log "Reactor watcher '${PROMISE_NAME}_failing' fired" +rm "$TESTFILE_REPAIRING" +wait_for_log "Reactor watcher '${PROMISE_NAME}_repairing' fired" + +# the outcomes of the previous events are only visible to the next event +rm "$CHECK_FILE" +wait_for_lines "$EVENTS_FILE" 2 + +[ -f "$CREATED_FILE" ] +[ "$(cut -d' ' -f3- "$EVENTS_FILE" | sort)" = "$(printf 'failing_failed\nrepairing_repaired')" ] diff --git a/tests/reactor/tests/00_basics/019_override_ifelapsed.cf b/tests/reactor/tests/00_basics/019_override_ifelapsed.cf new file mode 100644 index 00000000000..280980eed51 --- /dev/null +++ b/tests/reactor/tests/00_basics/019_override_ifelapsed.cf @@ -0,0 +1,52 @@ +####################################################### +# +# Test that the ifelapsed of 0 that cf-reactor uses by default can be +# overridden by the promises of the "then" bundles +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +body action if_elapsed(minutes) +{ + ifelapsed => "$(minutes)"; +} + +bundle agent record(file_default, file_ifelapsed, msg) +{ + reports: + # Must come before the report below: once that one is written, this one + # has already either been written or skipped because of its ifelapsed + "$(msg)" + report_to_file => "$(file_ifelapsed)", + action => if_elapsed("60"); + + "$(msg)" report_to_file => "$(file_default)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file_default" string => getenv("EVENTS_FILE_DEFAULT", 999); + "events_file_ifelapsed" string => getenv("EVENTS_FILE_IFELAPSED", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => record( + "$(events_file_default)", + "$(events_file_ifelapsed)", + "$(test) was deleted" + ); +} diff --git a/tests/reactor/tests/00_basics/019_override_ifelapsed.sh b/tests/reactor/tests/00_basics/019_override_ifelapsed.sh new file mode 100644 index 00000000000..a1dad6c47d7 --- /dev/null +++ b/tests/reactor/tests/00_basics/019_override_ifelapsed.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +# cf-reactor defaults ifelapsed to 0, but it can be overridden + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE_DEFAULT="$WORKDIR/tmp/events_default" +EVENTS_FILE_IFELAPSED="$WORKDIR/tmp/events_ifelapsed" +export TESTFILE EVENTS_FILE_DEFAULT EVENTS_FILE_IFELAPSED + +# create initial file +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE_DEFAULT" 1 +wait_for_lines "$EVENTS_FILE_IFELAPSED" 1 +# the reports are written without a timestamp +[ "$(cat "$EVENTS_FILE_DEFAULT")" = "$TESTFILE was deleted" ] +[ "$(cat "$EVENTS_FILE_IFELAPSED")" = "$TESTFILE was deleted" ] + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" + +# with the default ifelapsed of 0, the report is made again, but not the one +# with an ifelapsed of 60 minutes +wait_for_lines "$EVENTS_FILE_DEFAULT" 2 +wait_for_lines "$EVENTS_FILE_IFELAPSED" 1 +[ "$(sed -n 2p "$EVENTS_FILE_DEFAULT")" = "$TESTFILE was deleted" ] diff --git a/tests/reactor/tests/00_basics/020_policy_reload.cf b/tests/reactor/tests/00_basics/020_policy_reload.cf new file mode 100644 index 00000000000..082b9bcb833 --- /dev/null +++ b/tests/reactor/tests/00_basics/020_policy_reload.cf @@ -0,0 +1,37 @@ +####################################################### +# +# Test that a watcher keeps the state of its event over a policy reload, so +# that an event happening while reloading is detected +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "reload_marker" string => getenv("RELOAD_MARKER", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + # Evaluated on start and on every policy reload, after the watchers were + # set aside: tells the test it can change the file, and keeps the watchers + # paused long enough for it to do so + "slow" + string => execresult("touch '$(reload_marker)' && sleep 3", "useshell"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/020_policy_reload.sh b/tests/reactor/tests/00_basics/020_policy_reload.sh new file mode 100644 index 00000000000..48348e72338 --- /dev/null +++ b/tests/reactor/tests/00_basics/020_policy_reload.sh @@ -0,0 +1,83 @@ +#!/bin/sh + +# check that a watcher keeps the state of its event (whether the file existed +# at its last check) over a policy reload, i.e. that what the first check after +# the reload sees is compared to the state from before the reload, so that an +# event is neither missed nor reported twice: +# 1. last check: exists, reload, file deleted -> next check: event +# 2. last check: missing, reload, still missing -> next check: no event, +# the deletion was already reported +# 3. last check: missing, reload, file created -> next check: no event + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +RELOAD_MARKER="$WORKDIR/tmp/reload_marker" +export TESTFILE EVENTS_FILE RELOAD_MARKER + +# Reload the policy and wait until its watchers are set aside, which they stay +# for 3 seconds, see the "slow" vars promise. The file can't be checked until +# then, so changing it now is only seen by the watcher of the reloaded policy. +reload_and_wait_for_paused_watchers() { + # created when the policy was last evaluated + rm "$RELOAD_MARKER" + + kill -HUP "$reactor_pid" + wait_for_log "Rereading policy file" + + if ! timeout 60 sh -c 'until [ -f "$1" ]; do sleep 0.1; done' _ "$RELOAD_MARKER" + then + echo "Timed out waiting for the policy to be reloaded, reactor log:" >&2 + cat "$LOG" >&2 + exit 1 + fi +} + +# create initial file +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 +wait_for_log "File '$TESTFILE' is present, watching for its deletion" + +# 1. last check: exists. Without the state from before the reload, the first +# check after it would only find the file missing, which is not an event. +reload_and_wait_for_paused_watchers +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "$TESTFILE was deleted" + +# 2. last check: missing (the deletion above, already reported). Without the +# state from before the reload, a watcher assuming the file existed would +# report the deletion again. +rm "$RELOAD_MARKER" +kill -HUP "$reactor_pid" +wait_for_log "Rereading policy file" +# the watchers of the reloaded policy are registered and checked again +wait_for_log "cf-reactor event watcher has 1 watcher(s)" + +# Proving that something doesn't happen needs a wait, give it a couple of +# checks +sleep 2 +wait_for_lines "$EVENTS_FILE" 1 + +# 3. last check: missing. The file appearing is not an event, the watcher only +# starts watching for its deletion again. +reload_and_wait_for_paused_watchers +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" + +# An event would have been queued by the check that found the file present, +# give it a couple of checks to be handled +sleep 2 +wait_for_lines "$EVENTS_FILE" 1 + +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "$TESTFILE was deleted" diff --git a/tests/reactor/tests/00_basics/021_no_file.cf b/tests/reactor/tests/00_basics/021_no_file.cf new file mode 100644 index 00000000000..4edb8670d74 --- /dev/null +++ b/tests/reactor/tests/00_basics/021_no_file.cf @@ -0,0 +1,30 @@ +####################################################### +# +# Test a file_deleted event on a file that doesn't exist yet when the watcher +# is set up +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/021_no_file.sh b/tests/reactor/tests/00_basics/021_no_file.sh new file mode 100644 index 00000000000..2a96db24c6a --- /dev/null +++ b/tests/reactor/tests/00_basics/021_no_file.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +# check that a watcher set up while its file doesn't exist doesn't fire, and +# fires once the file is created and then deleted + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# no initial file + +start_reactor "$POLICY_FILE" + +# the watcher is set up, its check on registration found the file missing +wait_for_watchers 1 + +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" + +# the missing file at the first checks was not an event: those checks were +# made at least a second before the one which found the file present +if grep -F "Reactor watcher '$PROMISE_NAME' fired" "$LOG" || [ -e "$EVENTS_FILE" ] +then + echo "Watcher fired while the file didn't exist yet, reactor log:" >&2 + cat "$LOG" >&2 + exit 1 +fi + +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "$TESTFILE was deleted" From 5873ff4b101032171c58cfe135cec1b12698d4ee Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Wed, 21 Oct 2026 14:55:59 +0200 Subject: [PATCH 05/10] Added test for the file_deleted event type Added a folder to cover all event types with test. Starting with file_deleted. Signed-off-by: Victor Moene --- .../tests/01_event_types/001_file_deleted.cf | 48 ++++++++++++ .../tests/01_event_types/001_file_deleted.sh | 73 +++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 tests/reactor/tests/01_event_types/001_file_deleted.cf create mode 100644 tests/reactor/tests/01_event_types/001_file_deleted.sh diff --git a/tests/reactor/tests/01_event_types/001_file_deleted.cf b/tests/reactor/tests/01_event_types/001_file_deleted.cf new file mode 100644 index 00000000000..884afbfcfcc --- /dev/null +++ b/tests/reactor/tests/01_event_types/001_file_deleted.cf @@ -0,0 +1,48 @@ +####################################################### +# +# Test what is a deletion for a file_deleted event +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "file_a" string => getenv("FILE_A", 999); + "file_b" string => getenv("FILE_B", 999); + "file_c" string => getenv("FILE_C", 999); + "file_d" string => getenv("FILE_D", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + # a regular file + "$(promise_name)_A" + when => deleted("$(file_a)"), + then => record_event("$(events_file)", "A deleted"); + + # a file in a directory + "$(promise_name)_B" + when => deleted("$(file_b)"), + then => record_event("$(events_file)", "B deleted"); + + # a symlink + "$(promise_name)_C" + when => deleted("$(file_c)"), + then => record_event("$(events_file)", "C deleted"); + + # a directory + "$(promise_name)_D" + when => deleted("$(file_d)"), + then => record_event("$(events_file)", "D deleted"); +} diff --git a/tests/reactor/tests/01_event_types/001_file_deleted.sh b/tests/reactor/tests/01_event_types/001_file_deleted.sh new file mode 100644 index 00000000000..aa5c7f08544 --- /dev/null +++ b/tests/reactor/tests/01_event_types/001_file_deleted.sh @@ -0,0 +1,73 @@ +#!/bin/sh + +# check what is a deletion for a file_deleted event, which only looks at +# whether the path exists, without following it if it is a symlink: +# A. a regular file: modifying it is not a deletion, removing it is +# B. a file whose parent turns into a regular file (ENOTDIR) is deleted +# C. a symlink: removing its target is not a deletion, removing it is +# D. a directory: removing it is a deletion + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +FILE_A="$WORKDIR/tmp/file_a" +DIR_B="$WORKDIR/tmp/dir_b" +FILE_B="$DIR_B/file_b" +TARGET_C="$WORKDIR/tmp/target_c" +FILE_C="$WORKDIR/tmp/link_c" +FILE_D="$WORKDIR/tmp/dir_d" +EVENTS_FILE="$WORKDIR/tmp/events" +export FILE_A FILE_B FILE_C FILE_D EVENTS_FILE + +# create initial files +touch "$FILE_A" +mkdir "$DIR_B" +touch "$FILE_B" +touch "$TARGET_C" +ln -s "$TARGET_C" "$FILE_C" +mkdir "$FILE_D" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 4 + +# checked on registration, in the order of the promises +wait_for_log "File '$FILE_A' is present, watching for its deletion" +wait_for_log "File '$FILE_B' is present, watching for its deletion" +wait_for_log "File '$FILE_C' is present, watching for its deletion" +wait_for_log "File '$FILE_D' is present, watching for its deletion" + +# not deletions +echo "modified" >> "$FILE_A" +touch "$FILE_A" +chmod 600 "$FILE_A" +rm "$TARGET_C" + +# give the watchers a couple of checks to see them, in case they would fire. +# Proving that something doesn't happen needs a wait. +sleep 2 +if [ -e "$EVENTS_FILE" ] +then + echo "Fired on something that is not a deletion:" >&2 + cat "$EVENTS_FILE" >&2 + exit 1 +fi + +# deletions +rm "$FILE_A" + +# make the parent of the file a regular file, so that its path has a regular +# file as directory (ENOTDIR). A check right between the two commands would +# see it missing (ENOENT) instead, which is also a deletion. +rm -r "$DIR_B" +touch "$DIR_B" + +rm "$FILE_C" +rmdir "$FILE_D" + +# each fired once +wait_for_lines "$EVENTS_FILE" 4 +[ "$(cut -d' ' -f3- "$EVENTS_FILE" | sort)" = "$(printf 'A deleted\nB deleted\nC deleted\nD deleted')" ] From 7f5909b72354fcae0e8157e04524770333f594f0 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Wed, 21 Oct 2026 15:20:07 +0200 Subject: [PATCH 06/10] Made cf-reactor edit_line bundles cf-reactor should parse edit_line bundles so it can use them in the "then" bundles run on reaction to events Signed-off-by: Victor Moene --- libpromises/cf3parse_logic.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libpromises/cf3parse_logic.h b/libpromises/cf3parse_logic.h index 550d5e4cf44..56a71005f24 100644 --- a/libpromises/cf3parse_logic.h +++ b/libpromises/cf3parse_logic.h @@ -230,11 +230,13 @@ static bool RelevantBundle(const char *agent, const char *blocktype) return true; } - /* Here are some additional bundle types handled by cfAgent */ + /* Here are some additional bundle types handled by cfAgent, and by + * cf-reactor for the agent bundles it runs */ Item *ip = SplitString("edit_line,edit_xml", ','); - if (strcmp(agent, CF_AGENTTYPES[AGENT_TYPE_AGENT]) == 0) + if (StringEqual(agent, CF_AGENTTYPES[AGENT_TYPE_AGENT]) || + StringEqual(agent, CF_AGENTTYPES[AGENT_TYPE_REACTOR])) { if (IsItemIn(ip, blocktype)) { From 2e24324a7197320a6742689f2fe9e050a80d885c Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Wed, 21 Oct 2026 15:21:04 +0200 Subject: [PATCH 07/10] Fixed edit promises being skipped on events less than a minute apart The promises of edit_line and edit_xml bundles always used CF_EDIT_IFELAPSED (1 minute) as their ifelapsed, ignoring the default ifelapsed. cf-reactor sets the default to 0 for the bundles it runs on events, so a file edited by an event was not edited again by another event less than a minute later. Edit promises now use the lower of the two (EDIT_IFELAPSED). Signed-off-by: Victor Moene --- cf-agent/files_edit.h | 5 +++++ cf-agent/files_editline.c | 8 ++++---- cf-agent/files_editxml.c | 16 ++++++++-------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/cf-agent/files_edit.h b/cf-agent/files_edit.h index eb3a243ca64..91c1d97eb5d 100644 --- a/cf-agent/files_edit.h +++ b/cf-agent/files_edit.h @@ -41,6 +41,11 @@ copy equal to the copied template file - not the copied + edited file. */ +/* The ifelapsed of the promises of edit bundles: CF_EDIT_IFELAPSED, unless the + * default ifelapsed is lower, e.g. 0 for the bundles cf-reactor runs on events, + * which may happen less than a minute apart */ +#define EDIT_IFELAPSED MIN(CF_EDIT_IFELAPSED, VIFELAPSED) + typedef struct { char *filename; diff --git a/cf-agent/files_editline.c b/cf-agent/files_editline.c index 39ecde4e457..ffe76d53047 100644 --- a/cf-agent/files_editline.c +++ b/cf-agent/files_editline.c @@ -373,7 +373,7 @@ static PromiseResult VerifyLineDeletions(EvalContext *ctx, const Promise *pp, Ed char lockname[CF_BUFSIZE]; Attributes a = GetDeletionAttributes(ctx, pp); - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; if (!SanityCheckDeletions(&a, pp)) { @@ -453,7 +453,7 @@ static PromiseResult VerifyColumnEdits(EvalContext *ctx, const Promise *pp, Edit char lockname[CF_BUFSIZE]; Attributes a = GetColumnAttributes(ctx, pp); - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; if (a.column.column_separator == NULL) { @@ -547,7 +547,7 @@ static PromiseResult VerifyPatterns(EvalContext *ctx, const Promise *pp, EditCon /* Are we working in a restricted region? */ Attributes a = GetReplaceAttributes(ctx, pp); - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; if (!a.replace.replace_value) { @@ -733,7 +733,7 @@ static PromiseResult VerifyLineInsertions(EvalContext *ctx, const Promise *pp, E Attributes a = GetInsertionAttributes(ctx, pp); int allow_multi_lines = StringEqual(a.sourcetype, "preserve_all_lines"); - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; if (!SanityCheckInsertions(&a)) { diff --git a/cf-agent/files_editxml.c b/cf-agent/files_editxml.c index 0bc01b5b8a2..545db719396 100644 --- a/cf-agent/files_editxml.c +++ b/cf-agent/files_editxml.c @@ -353,7 +353,7 @@ static bool VerifyXPathBuild(EvalContext *ctx, const Attributes *attr, const Pro CfLock thislock; char lockname[CF_BUFSIZE], rawxpath[CF_BUFSIZE] = { 0 }; - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; int ret = snprintf(rawxpath, sizeof(rawxpath), "%s", a.xml.havebuildxpath ? a.xml.build_xpath : pp->promiser); @@ -417,7 +417,7 @@ static PromiseResult VerifyTreeDeletions(EvalContext *ctx, const Attributes *att CfLock thislock; char lockname[CF_BUFSIZE]; - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; if (!SanityCheckTreeDeletions(&a)) { @@ -497,7 +497,7 @@ static PromiseResult VerifyTreeInsertions(EvalContext *ctx, const Attributes *at CfLock thislock; char lockname[CF_BUFSIZE]; - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; PromiseResult result = PROMISE_RESULT_NOOP; if (!SanityCheckTreeInsertions(&a, edcontext)) @@ -590,7 +590,7 @@ static PromiseResult VerifyAttributeDeletions(EvalContext *ctx, const Attributes CfLock thislock; char lockname[CF_BUFSIZE]; - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; PromiseResult result = PROMISE_RESULT_NOOP; if (!SanityCheckAttributeDeletions(&a)) @@ -673,7 +673,7 @@ static PromiseResult VerifyAttributeSet(EvalContext *ctx, const Attributes *attr CfLock thislock; char lockname[CF_BUFSIZE]; - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; PromiseResult result = PROMISE_RESULT_NOOP; if (!SanityCheckAttributeSet(&a)) @@ -756,7 +756,7 @@ static PromiseResult VerifyTextDeletions(EvalContext *ctx, const Attributes *att CfLock thislock; char lockname[CF_BUFSIZE]; - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; PromiseResult result = PROMISE_RESULT_NOOP; if (!SanityCheckTextDeletions(&a)) @@ -839,7 +839,7 @@ static PromiseResult VerifyTextSet(EvalContext *ctx, const Attributes *attr, con CfLock thislock; char lockname[CF_BUFSIZE]; - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; PromiseResult result = PROMISE_RESULT_NOOP; if (!SanityCheckTextSet(&a)) @@ -922,7 +922,7 @@ static PromiseResult VerifyTextInsertions(EvalContext *ctx, const Attributes *at CfLock thislock; char lockname[CF_BUFSIZE]; - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; PromiseResult result = PROMISE_RESULT_NOOP; if (!SanityCheckTextInsertions(&a)) From af3ea2d94a09f0293f5dd524b828791a01ce627c Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Wed, 21 Oct 2026 15:24:17 +0200 Subject: [PATCH 08/10] Added tests for all promise types inside reactor "then" bundle run on event Signed-off-by: Victor Moene --- tests/reactor/lib.cf | 25 +++++ tests/reactor/run.sh | 18 ++- .../reactor/tests/02_then_bundles/001_meta.cf | 44 ++++++++ .../reactor/tests/02_then_bundles/001_meta.sh | 38 +++++++ .../reactor/tests/02_then_bundles/002_vars.cf | 40 +++++++ .../reactor/tests/02_then_bundles/002_vars.sh | 38 +++++++ .../tests/02_then_bundles/003_defaults.cf | 40 +++++++ .../tests/02_then_bundles/003_defaults.sh | 34 ++++++ .../tests/02_then_bundles/004_classes.cf | 47 ++++++++ .../tests/02_then_bundles/004_classes.sh | 37 +++++++ .../tests/02_then_bundles/006_files.cf | 48 ++++++++ .../tests/02_then_bundles/006_files.sh | 39 +++++++ .../tests/02_then_bundles/007_packages.cf | 50 +++++++++ .../tests/02_then_bundles/007_packages.sh | 104 ++++++++++++++++++ .../02_then_bundles/008_guest_environments.cf | 41 +++++++ .../02_then_bundles/008_guest_environments.sh | 41 +++++++ .../tests/02_then_bundles/009_methods.cf | 44 ++++++++ .../tests/02_then_bundles/009_methods.sh | 40 +++++++ .../tests/02_then_bundles/010_processes.cf | 39 +++++++ .../tests/02_then_bundles/010_processes.sh | 60 ++++++++++ .../tests/02_then_bundles/011_services.cf | 46 ++++++++ .../tests/02_then_bundles/011_services.sh | 35 ++++++ .../tests/02_then_bundles/012_commands.cf | 55 +++++++++ .../tests/02_then_bundles/012_commands.sh | 44 ++++++++ .../tests/02_then_bundles/013_storage.cf | 44 ++++++++ .../tests/02_then_bundles/013_storage.sh | 34 ++++++ .../tests/02_then_bundles/014_databases.cf | 49 +++++++++ .../tests/02_then_bundles/014_databases.sh | 34 ++++++ .../tests/02_then_bundles/015_reports.cf | 41 +++++++ .../tests/02_then_bundles/015_reports.sh | 38 +++++++ .../tests/02_then_bundles/016_custom.cf | 47 ++++++++ .../tests/02_then_bundles/016_custom.sh | 83 ++++++++++++++ .../tests/02_then_bundles/unsafe/005_users.cf | 41 +++++++ .../tests/02_then_bundles/unsafe/005_users.sh | 37 +++++++ 34 files changed, 1493 insertions(+), 2 deletions(-) create mode 100644 tests/reactor/tests/02_then_bundles/001_meta.cf create mode 100644 tests/reactor/tests/02_then_bundles/001_meta.sh create mode 100644 tests/reactor/tests/02_then_bundles/002_vars.cf create mode 100644 tests/reactor/tests/02_then_bundles/002_vars.sh create mode 100644 tests/reactor/tests/02_then_bundles/003_defaults.cf create mode 100644 tests/reactor/tests/02_then_bundles/003_defaults.sh create mode 100644 tests/reactor/tests/02_then_bundles/004_classes.cf create mode 100644 tests/reactor/tests/02_then_bundles/004_classes.sh create mode 100644 tests/reactor/tests/02_then_bundles/006_files.cf create mode 100644 tests/reactor/tests/02_then_bundles/006_files.sh create mode 100644 tests/reactor/tests/02_then_bundles/007_packages.cf create mode 100644 tests/reactor/tests/02_then_bundles/007_packages.sh create mode 100644 tests/reactor/tests/02_then_bundles/008_guest_environments.cf create mode 100644 tests/reactor/tests/02_then_bundles/008_guest_environments.sh create mode 100644 tests/reactor/tests/02_then_bundles/009_methods.cf create mode 100644 tests/reactor/tests/02_then_bundles/009_methods.sh create mode 100644 tests/reactor/tests/02_then_bundles/010_processes.cf create mode 100644 tests/reactor/tests/02_then_bundles/010_processes.sh create mode 100644 tests/reactor/tests/02_then_bundles/011_services.cf create mode 100644 tests/reactor/tests/02_then_bundles/011_services.sh create mode 100644 tests/reactor/tests/02_then_bundles/012_commands.cf create mode 100644 tests/reactor/tests/02_then_bundles/012_commands.sh create mode 100644 tests/reactor/tests/02_then_bundles/013_storage.cf create mode 100644 tests/reactor/tests/02_then_bundles/013_storage.sh create mode 100644 tests/reactor/tests/02_then_bundles/014_databases.cf create mode 100644 tests/reactor/tests/02_then_bundles/014_databases.sh create mode 100644 tests/reactor/tests/02_then_bundles/015_reports.cf create mode 100644 tests/reactor/tests/02_then_bundles/015_reports.sh create mode 100644 tests/reactor/tests/02_then_bundles/016_custom.cf create mode 100644 tests/reactor/tests/02_then_bundles/016_custom.sh create mode 100644 tests/reactor/tests/02_then_bundles/unsafe/005_users.cf create mode 100644 tests/reactor/tests/02_then_bundles/unsafe/005_users.sh diff --git a/tests/reactor/lib.cf b/tests/reactor/lib.cf index d1f11667a5a..e20aedc265c 100644 --- a/tests/reactor/lib.cf +++ b/tests/reactor/lib.cf @@ -5,3 +5,28 @@ bundle agent record_event(file, message) with => strftime("localtime", "%Y-%m-%d %H:%M:%S", now()), report_to_file => "$(file)"; } + +# Defines a class for the outcome of the promise it is used on: +# _kept, _repaired or _failed +body classes then_outcome(prefix) +{ + promise_kept => { "$(prefix)_kept" }; + promise_repaired => { "$(prefix)_repaired" }; + repair_failed => { "$(prefix)_failed" }; + repair_denied => { "$(prefix)_failed" }; + repair_timeout => { "$(prefix)_failed" }; +} + +# Records " " for the outcome classes of then_outcome(prefix) +# defined so far. Nothing is recorded if the promise was not evaluated (e.g. +# skipped because of a lock). +bundle agent record_then_outcome(file, prefix) +{ + vars: + "outcomes" slist => { "kept", "repaired", "failed" }; + + methods: + "$(prefix)_$(outcomes)" + if => "$(prefix)_$(outcomes)", + usebundle => record_event("$(file)", "$(prefix) $(outcomes)"); +} diff --git a/tests/reactor/run.sh b/tests/reactor/run.sh index 9612c34fcf1..17e21f198c5 100644 --- a/tests/reactor/run.sh +++ b/tests/reactor/run.sh @@ -1,15 +1,23 @@ #!/bin/sh -# Usage: run.sh +# Usage: run.sh [--unsafe] # # Must be run from the tests/reactor directory. # +# Tests in an unsafe/ directory modify the system they are running on, so they +# are only run with --unsafe. +# # The binaries are taken from $CF_BINDIR if it is set, otherwise from the # CFEngine build tree this directory is part of if they are built, otherwise # from /var/cfengine/bin. fail=0 +unsafe=0 +if [ "$1" = "--unsafe" ]; then + unsafe=1 +fi + banner() { name=" $(basename "$(dirname "$1")")/$(basename "$1") " width=72 @@ -52,7 +60,13 @@ for b in "$CF_REACTOR" "$CF_PROMISES"; do done echo "Using $CF_REACTOR and $CF_PROMISES" -for t in "$PWD"/tests/*/*.sh; do +for t in "$PWD"/tests/*/*.sh "$PWD"/tests/*/unsafe/*.sh; do + # skip the pattern itself if there are no unsafe tests + [ -f "$t" ] || continue + case "$t" in + */unsafe/*) [ "$unsafe" = 1 ] || continue ;; + esac + banner "$t" # create throwaway work dir diff --git a/tests/reactor/tests/02_then_bundles/001_meta.cf b/tests/reactor/tests/02_then_bundles/001_meta.cf new file mode 100644 index 00000000000..e43111b40e6 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/001_meta.cf @@ -0,0 +1,44 @@ +####################################################### +# +# Test meta promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +# Not named run_meta: the meta variables of a bundle whose name ends with +# "_meta" are in the scope of the bundle itself, not in _meta +bundle agent run_meta_promises(events_file, value_file) +{ + meta: + "value" string => readfile("$(value_file)"); + + methods: + "any" + usebundle => record_event( + "$(events_file)", "meta $(run_meta_promises_meta.value)" + ); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "value_file" string => getenv("VALUE_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => run_meta_promises("$(events_file)", "$(value_file)"); +} diff --git a/tests/reactor/tests/02_then_bundles/001_meta.sh b/tests/reactor/tests/02_then_bundles/001_meta.sh new file mode 100644 index 00000000000..f878cbbd33f --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/001_meta.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# check that meta promises in a "then" bundle are evaluated again on every +# event, in the _meta scope + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +VALUE_FILE="$WORKDIR/tmp/value" +export TESTFILE EVENTS_FILE VALUE_FILE + +printf "first" > "$VALUE_FILE" +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "meta first" + +printf "second" > "$VALUE_FILE" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "meta second" diff --git a/tests/reactor/tests/02_then_bundles/002_vars.cf b/tests/reactor/tests/02_then_bundles/002_vars.cf new file mode 100644 index 00000000000..e2aefbe08c4 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/002_vars.cf @@ -0,0 +1,40 @@ +####################################################### +# +# Test vars promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_vars(events_file, value_file) +{ + vars: + # execresult() is a cached function: the cache must not outlive an event + "value" string => execresult("/bin/cat $(value_file)", "noshell"); + + methods: + "any" usebundle => record_event("$(events_file)", "vars $(value)"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "value_file" string => getenv("VALUE_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => run_vars("$(events_file)", "$(value_file)"); +} diff --git a/tests/reactor/tests/02_then_bundles/002_vars.sh b/tests/reactor/tests/02_then_bundles/002_vars.sh new file mode 100644 index 00000000000..249bab50186 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/002_vars.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# check that vars promises in a "then" bundle are evaluated again on every +# event, without the value of a cached function from the previous one + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +VALUE_FILE="$WORKDIR/tmp/value" +export TESTFILE EVENTS_FILE VALUE_FILE + +printf "first" > "$VALUE_FILE" +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "vars first" + +printf "second" > "$VALUE_FILE" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "vars second" diff --git a/tests/reactor/tests/02_then_bundles/003_defaults.cf b/tests/reactor/tests/02_then_bundles/003_defaults.cf new file mode 100644 index 00000000000..0f604a5d507 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/003_defaults.cf @@ -0,0 +1,40 @@ +####################################################### +# +# Test defaults promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_defaults(events_file, msg) +{ + defaults: + "msg" + string => "default message", + if_match_regex => ""; + + methods: + "any" usebundle => record_event("$(events_file)", "defaults $(msg)"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => run_defaults("$(events_file)", ""); +} diff --git a/tests/reactor/tests/02_then_bundles/003_defaults.sh b/tests/reactor/tests/02_then_bundles/003_defaults.sh new file mode 100644 index 00000000000..c35646fe238 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/003_defaults.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# check that defaults promises in a "then" bundle are evaluated on every event + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" + +export TESTFILE EVENTS_FILE + +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "defaults default message" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "defaults default message" diff --git a/tests/reactor/tests/02_then_bundles/004_classes.cf b/tests/reactor/tests/02_then_bundles/004_classes.cf new file mode 100644 index 00000000000..0b7c32a8982 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/004_classes.cf @@ -0,0 +1,47 @@ +####################################################### +# +# Test classes promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_classes(events_file, marker) +{ + classes: + # returnszero() is a cached function: the cache must not outlive an event + "marker_exists" + expression => returnszero("test -f '$(marker)'", "useshell"); + + methods: + marker_exists:: + "any" + usebundle => record_event("$(events_file)", "classes marker exists"); + + !marker_exists:: + "any" + usebundle => record_event("$(events_file)", "classes marker missing"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "marker" string => getenv("MARKER_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => run_classes("$(events_file)", "$(marker)"); +} diff --git a/tests/reactor/tests/02_then_bundles/004_classes.sh b/tests/reactor/tests/02_then_bundles/004_classes.sh new file mode 100644 index 00000000000..1200099aa0a --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/004_classes.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +# check that classes promises in a "then" bundle are evaluated again on +# every event, without the value of a cached function from the previous one + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +MARKER_FILE="$WORKDIR/tmp/marker" +export TESTFILE EVENTS_FILE MARKER_FILE + +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "classes marker missing" + +touch "$MARKER_FILE" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "classes marker exists" diff --git a/tests/reactor/tests/02_then_bundles/006_files.cf b/tests/reactor/tests/02_then_bundles/006_files.cf new file mode 100644 index 00000000000..90a482eee6f --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/006_files.cf @@ -0,0 +1,48 @@ +####################################################### +# +# Test files promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_files(file, line) +{ + files: + "$(file)" + create => "true", + edit_line => insert_line("$(line)"), + classes => then_outcome("files"); +} + +bundle edit_line insert_line(line) +{ + insert_lines: + "$(line)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "created" string => getenv("CREATED_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + run_files("$(created)", "created on event"), + record_then_outcome("$(events_file)", "files"), + }; +} diff --git a/tests/reactor/tests/02_then_bundles/006_files.sh b/tests/reactor/tests/02_then_bundles/006_files.sh new file mode 100644 index 00000000000..2030d26e9cc --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/006_files.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +# check that files promises in a "then" bundle are evaluated again on every +# event: the file deleted after the first one is created again + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +CREATED_FILE="$WORKDIR/tmp/created" +export TESTFILE EVENTS_FILE CREATED_FILE + +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "files repaired" +[ "$(cat "$CREATED_FILE")" = "created on event" ] + +rm "$CREATED_FILE" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "files repaired" +[ "$(cat "$CREATED_FILE")" = "created on event" ] diff --git a/tests/reactor/tests/02_then_bundles/007_packages.cf b/tests/reactor/tests/02_then_bundles/007_packages.cf new file mode 100644 index 00000000000..84bcd32de98 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/007_packages.cf @@ -0,0 +1,50 @@ +####################################################### +# +# Test packages promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +# Queries the installed packages on every evaluation (query_installed_ifelapsed +# of 0), so the package removed between events is installed again +body package_module reactor_test +{ + query_installed_ifelapsed => "0"; + query_updates_ifelapsed => "0"; + default_options => { "$(test.installed)" }; +} + +bundle agent run_packages +{ + packages: + "reactor_test_pkg" + policy => "present", + package_module => reactor_test, + classes => then_outcome("packages"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "installed" string => getenv("INSTALLED_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + "run_packages", record_then_outcome("$(events_file)", "packages") + }; +} diff --git a/tests/reactor/tests/02_then_bundles/007_packages.sh b/tests/reactor/tests/02_then_bundles/007_packages.sh new file mode 100644 index 00000000000..3de0843da9a --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/007_packages.sh @@ -0,0 +1,104 @@ +#!/bin/sh + +# check that packages promises in a "then" bundle are evaluated again on +# every event: the package removed after the first one is installed again + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +INSTALLED_FILE="$WORKDIR/tmp/installed" +export TESTFILE EVENTS_FILE INSTALLED_FILE + +# A package module installing packages by adding them to the file given as +# option, see tests/acceptance/07_packages/package_module_path.cf.module +mkdir -p "$WORKDIR/modules/packages" +cat > "$WORKDIR/modules/packages/reactor_test" <<'MODULE' +#!/bin/sh + +set -e + +remove_prefix() +{ + echo "$1" | sed "s/$2//" +} + +case "$1" in + supports-api-version) + echo 1 + ;; + get-package-data) + while read line; do + case "$line" in + File=*) + echo PackageType=repo + echo Name=`remove_prefix "${line}" "File="` + ;; + esac + done + ;; + list-installed) + while read line; do + case "$line" in + options=*) + OUTPUT=`remove_prefix "${line}" "options="` + ;; + esac + done + if [ -f "$OUTPUT" ]; then + cat "$OUTPUT" + fi + ;; + list-*) + cat > /dev/null + ;; + repo-install) + while read line; do + case "$line" in + options=*) + OUTPUT=`remove_prefix "${line}" "options="` + ;; + Name=*) + NAME=`remove_prefix "${line}" "Name="` + ;; + esac + done + echo "Name=$NAME" >> "$OUTPUT" + echo "Version=1.0" >> "$OUTPUT" + echo "Architecture=generic" >> "$OUTPUT" + ;; + *) + exit 1 + ;; +esac +MODULE +chmod 700 "$WORKDIR/modules/packages/reactor_test" + +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "packages repaired" +grep -x "Name=reactor_test_pkg" "$INSTALLED_FILE" + +# remove the package +rm "$INSTALLED_FILE" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "packages repaired" +grep -x "Name=reactor_test_pkg" "$INSTALLED_FILE" diff --git a/tests/reactor/tests/02_then_bundles/008_guest_environments.cf b/tests/reactor/tests/02_then_bundles/008_guest_environments.cf new file mode 100644 index 00000000000..1297334a885 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/008_guest_environments.cf @@ -0,0 +1,41 @@ +####################################################### +# +# Test guest_environments promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_guest_environments +{ + guest_environments: + # the guest that the "test" hypervisor of libvirt (test:///default) runs. + # Without libvirt, guest_environments promises are kept without doing + # anything, and without defining the classes of their outcome, so there's + # no classes body here: the test looks at the log instead. + "test" + environment_type => "test", + environment_host => "$(sys.host)", + environment_state => "running"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => "run_guest_environments"; +} diff --git a/tests/reactor/tests/02_then_bundles/008_guest_environments.sh b/tests/reactor/tests/02_then_bundles/008_guest_environments.sh new file mode 100644 index 00000000000..8b34adbe4c1 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/008_guest_environments.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +# check that guest_environments promises in a "then" bundle are evaluated on +# every event. Without libvirt, they don't define the classes of their +# outcome, so this looks at the log instead. + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" + +export TESTFILE + +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +# The promise is evaluated, and logged, on every pass of the bundle: wait for +# the end of the bundle, so that the next event only sees the logs of its run +wait_for_evaluation() { + wait_for_log "P: BEGIN guest_environments promise (test)" + wait_for_log "A: Promise was KEPT" + wait_for_log "B: END bundle run_guest_environments" +} + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_evaluation + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_evaluation diff --git a/tests/reactor/tests/02_then_bundles/009_methods.cf b/tests/reactor/tests/02_then_bundles/009_methods.cf new file mode 100644 index 00000000000..ab373e2a142 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/009_methods.cf @@ -0,0 +1,44 @@ +####################################################### +# +# Test methods promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_methods(events_file, value_file) +{ + methods: + "any" + usebundle => record_event("$(events_file)", "methods $(value)"), + classes => then_outcome("methods"); + + vars: + "value" string => readfile("$(value_file)"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "value_file" string => getenv("VALUE_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + run_methods("$(events_file)", "$(value_file)"), + record_then_outcome("$(events_file)", "methods"), + }; +} diff --git a/tests/reactor/tests/02_then_bundles/009_methods.sh b/tests/reactor/tests/02_then_bundles/009_methods.sh new file mode 100644 index 00000000000..fede02ddc5a --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/009_methods.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +# check that methods promises in a "then" bundle are evaluated again on every +# event, with the arguments of the event + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +VALUE_FILE="$WORKDIR/tmp/value" +export TESTFILE EVENTS_FILE VALUE_FILE + +printf "first" > "$VALUE_FILE" +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 1 "methods first" +check_event "$EVENTS_FILE" 2 "methods kept" + +printf "second" > "$VALUE_FILE" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 4 +check_event "$EVENTS_FILE" 3 "methods second" +check_event "$EVENTS_FILE" 4 "methods kept" diff --git a/tests/reactor/tests/02_then_bundles/010_processes.cf b/tests/reactor/tests/02_then_bundles/010_processes.cf new file mode 100644 index 00000000000..f25585e625b --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/010_processes.cf @@ -0,0 +1,39 @@ +####################################################### +# +# Test processes promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_processes +{ + processes: + "sleep 978654" + signals => { "term" }, + classes => then_outcome("processes"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + "run_processes", record_then_outcome("$(events_file)", "processes") + }; +} diff --git a/tests/reactor/tests/02_then_bundles/010_processes.sh b/tests/reactor/tests/02_then_bundles/010_processes.sh new file mode 100644 index 00000000000..b653eb059e9 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/010_processes.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +# check that processes promises in a "then" bundle are evaluated again on +# every event, with the process table of the event: the process started +# after the first one is killed by the second one + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +PID_FILE="$WORKDIR/tmp/pid" +export TESTFILE EVENTS_FILE PID_FILE + +# Start a process to be matched by the processes promise, detached from this +# shell so that it doesn't stay a zombie of it once killed +start_process() { + ( sleep 978654 & echo $! > "$PID_FILE" ) +} + +wait_for_process_killed() { + if ! timeout 60 sh -c 'while kill -0 "$1" 2>/dev/null; do sleep 0.1; done' _ "$(cat "$PID_FILE")" + then + echo "Process $(cat "$PID_FILE") not killed, reactor log:" >&2 + cat "$LOG" >&2 + return 1 + fi +} + +start_process +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +# like in cf-agent, killing the process defines both classes +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 1 "processes kept" +check_event "$EVENTS_FILE" 2 "processes repaired" +wait_for_process_killed + +# a new process, not in the process table of the first event +start_process + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 4 +check_event "$EVENTS_FILE" 3 "processes kept" +check_event "$EVENTS_FILE" 4 "processes repaired" +wait_for_process_killed diff --git a/tests/reactor/tests/02_then_bundles/011_services.cf b/tests/reactor/tests/02_then_bundles/011_services.cf new file mode 100644 index 00000000000..900f05466a6 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/011_services.cf @@ -0,0 +1,46 @@ +####################################################### +# +# Test services promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +body service_method record_service(events_file, service) +{ + service_bundle => record_event( + "$(events_file)", "services $(service) $(this.service_policy)" + ); +} + +bundle agent run_services(events_file) +{ + services: + "reactor_test_service" + service_policy => "start", + service_method => record_service( + "$(events_file)", "reactor_test_service" + ); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => run_services("$(events_file)"); +} diff --git a/tests/reactor/tests/02_then_bundles/011_services.sh b/tests/reactor/tests/02_then_bundles/011_services.sh new file mode 100644 index 00000000000..d39059383ee --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/011_services.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +# check that services promises in a "then" bundle are evaluated on every +# event, running the bundle of their service method + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" + +export TESTFILE EVENTS_FILE + +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "services reactor_test_service start" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "services reactor_test_service start" diff --git a/tests/reactor/tests/02_then_bundles/012_commands.cf b/tests/reactor/tests/02_then_bundles/012_commands.cf new file mode 100644 index 00000000000..55579d73934 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/012_commands.cf @@ -0,0 +1,55 @@ +####################################################### +# +# Test commands promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +body contain in_shell +{ + useshell => "useshell"; +} + +bundle agent run_commands(events_file, output, module_file) +{ + commands: + "echo ran >> '$(output)'" + contain => in_shell, + classes => then_outcome("commands"); + + # defines reactor_test.value from the output of the command + "/bin/cat $(module_file)" module => "true"; + + reports: + "$(with): commands module value $(reactor_test.value)" + with => strftime("localtime", "%Y-%m-%d %H:%M:%S", now()), + report_to_file => "$(events_file)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "output" string => getenv("OUTPUT_FILE", 999); + "module_file" string => getenv("MODULE_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + run_commands("$(events_file)", "$(output)", "$(module_file)"), + record_then_outcome("$(events_file)", "commands"), + }; +} diff --git a/tests/reactor/tests/02_then_bundles/012_commands.sh b/tests/reactor/tests/02_then_bundles/012_commands.sh new file mode 100644 index 00000000000..353994ea6c8 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/012_commands.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +# check that commands promises in a "then" bundle are run again on every +# event, and that the variables of a module command are the ones of its +# output of the event + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +OUTPUT_FILE="$WORKDIR/tmp/output" +MODULE_FILE="$WORKDIR/tmp/module" +export TESTFILE EVENTS_FILE OUTPUT_FILE MODULE_FILE + +printf "^context=reactor_test\n=value=first\n" > "$MODULE_FILE" +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 1 "commands module value first" +check_event "$EVENTS_FILE" 2 "commands repaired" +[ "$(cat "$OUTPUT_FILE")" = "ran" ] + +printf "^context=reactor_test\n=value=second\n" > "$MODULE_FILE" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 4 +check_event "$EVENTS_FILE" 3 "commands module value second" +check_event "$EVENTS_FILE" 4 "commands repaired" +[ "$(cat "$OUTPUT_FILE")" = "$(printf 'ran\nran')" ] diff --git a/tests/reactor/tests/02_then_bundles/013_storage.cf b/tests/reactor/tests/02_then_bundles/013_storage.cf new file mode 100644 index 00000000000..9968a0e9c14 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/013_storage.cf @@ -0,0 +1,44 @@ +####################################################### +# +# Test storage promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +body volume any_free_space +{ + freespace => "1k"; +} + +bundle agent run_storage +{ + storage: + "$(sys.workdir)" + volume => any_free_space, + classes => then_outcome("storage"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + "run_storage", record_then_outcome("$(events_file)", "storage") + }; +} diff --git a/tests/reactor/tests/02_then_bundles/013_storage.sh b/tests/reactor/tests/02_then_bundles/013_storage.sh new file mode 100644 index 00000000000..274973fc09b --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/013_storage.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# check that storage promises in a "then" bundle are evaluated on every event + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" + +export TESTFILE EVENTS_FILE + +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "storage kept" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "storage kept" diff --git a/tests/reactor/tests/02_then_bundles/014_databases.cf b/tests/reactor/tests/02_then_bundles/014_databases.cf new file mode 100644 index 00000000000..881c8cda5e2 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/014_databases.cf @@ -0,0 +1,49 @@ +####################################################### +# +# Test databases promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +# No database server is running, so the promise can't be kept, but it is +# evaluated +body database_server no_server +{ + db_server_owner => "nobody"; + db_server_password => ""; + db_server_host => "localhost"; + db_server_type => "postgres"; + db_server_connection_db => "postgres"; +} + +bundle agent run_databases +{ + databases: + "reactor_test" + database_operation => "create", + database_type => "sql", + database_server => no_server; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => "run_databases"; +} diff --git a/tests/reactor/tests/02_then_bundles/014_databases.sh b/tests/reactor/tests/02_then_bundles/014_databases.sh new file mode 100644 index 00000000000..31aaef4928f --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/014_databases.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# check that databases promises in a "then" bundle are evaluated on every +# event. Like in cf-agent, no outcome classes are defined when the server +# can't be connected to, so this looks for the error of each evaluation. + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" + +export TESTFILE EVENTS_FILE + +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_log "Could not connect to the sql_db server for 'reactor_test'" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_log "Could not connect to the sql_db server for 'reactor_test'" diff --git a/tests/reactor/tests/02_then_bundles/015_reports.cf b/tests/reactor/tests/02_then_bundles/015_reports.cf new file mode 100644 index 00000000000..7d962d2129e --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/015_reports.cf @@ -0,0 +1,41 @@ +####################################################### +# +# Test reports promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_reports(events_file, value_file) +{ + vars: + "value" string => readfile("$(value_file)"); + + reports: + "$(with): reports $(value)" + with => strftime("localtime", "%Y-%m-%d %H:%M:%S", now()), + report_to_file => "$(events_file)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "value_file" string => getenv("VALUE_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => run_reports("$(events_file)", "$(value_file)"); +} diff --git a/tests/reactor/tests/02_then_bundles/015_reports.sh b/tests/reactor/tests/02_then_bundles/015_reports.sh new file mode 100644 index 00000000000..0bb6e198d83 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/015_reports.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# check that reports promises in a "then" bundle are made again on every +# event, with the values of the event + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +VALUE_FILE="$WORKDIR/tmp/value" +export TESTFILE EVENTS_FILE VALUE_FILE + +printf "first" > "$VALUE_FILE" +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "reports first" + +printf "second" > "$VALUE_FILE" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "reports second" diff --git a/tests/reactor/tests/02_then_bundles/016_custom.cf b/tests/reactor/tests/02_then_bundles/016_custom.cf new file mode 100644 index 00000000000..7238fd37a83 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/016_custom.cf @@ -0,0 +1,47 @@ +####################################################### +# +# Test custom promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +promise agent reactor_test +{ + interpreter => "/bin/sh"; + path => "$(sys.workdir)/modules/promises/reactor_test.sh"; +} + +bundle agent run_custom(output) +{ + reactor_test: + "$(output)" + message => "ran", + classes => then_outcome("custom"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "output" string => getenv("OUTPUT_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + run_custom("$(output)"), + record_then_outcome("$(events_file)", "custom"), + }; +} diff --git a/tests/reactor/tests/02_then_bundles/016_custom.sh b/tests/reactor/tests/02_then_bundles/016_custom.sh new file mode 100644 index 00000000000..1ec1418feae --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/016_custom.sh @@ -0,0 +1,83 @@ +#!/bin/sh + +# check that custom promises in a "then" bundle are evaluated again on every +# event, with a promise module started for the event and terminated after it + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +OUTPUT_FILE="$WORKDIR/tmp/output" +MODULE_LOG="$WORKDIR/tmp/module_log" +export TESTFILE EVENTS_FILE OUTPUT_FILE MODULE_LOG + +# A promise module appending the message attribute to the promiser file, and +# its start and termination to $MODULE_LOG +mkdir -p "$WORKDIR/modules/promises" +cat > "$WORKDIR/modules/promises/reactor_test.sh" <<'MODULE' +echo "started" >> "$MODULE_LOG" + +# skip the header of the agent, up to the empty line +while read -r line && [ -n "$line" ]; do :; done +echo "reactor_test 0.0.1 v1 line_based" +echo "" + +while true; do + operation="" + promiser="" + message="" + while read -r line && [ -n "$line" ]; do + case "$line" in + operation=*) operation="${line#*=}" ;; + promiser=*) promiser="${line#*=}" ;; + attribute_message=*) message="${line#*=}" ;; + esac + done + + case "$operation" in + validate_promise) + result="valid" ;; + evaluate_promise) + if echo "$message" >> "$promiser"; then + result="repaired" + else + result="not_kept" + fi ;; + terminate) + echo "terminated" >> "$MODULE_LOG" + printf 'operation=terminate\nresult=success\n\n' + exit 0 ;; + *) + result="error" ;; + esac + printf 'operation=%s\nresult=%s\n\n' "$operation" "$result" +done +MODULE + +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "custom repaired" +[ "$(cat "$OUTPUT_FILE")" = "ran" ] +[ "$(cat "$MODULE_LOG")" = "$(printf 'started\nterminated')" ] + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "custom repaired" +[ "$(cat "$OUTPUT_FILE")" = "$(printf 'ran\nran')" ] +[ "$(cat "$MODULE_LOG")" = "$(printf 'started\nterminated\nstarted\nterminated')" ] diff --git a/tests/reactor/tests/02_then_bundles/unsafe/005_users.cf b/tests/reactor/tests/02_then_bundles/unsafe/005_users.cf new file mode 100644 index 00000000000..1efa2655cbb --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/unsafe/005_users.cf @@ -0,0 +1,41 @@ +####################################################### +# +# Test users promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_users(user) +{ + users: + # the user running the test, so that it is kept without being root + "$(user)" + policy => "present", + classes => then_outcome("users"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "user" string => getenv("TEST_USER", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + run_users("$(user)"), record_then_outcome("$(events_file)", "users") + }; +} diff --git a/tests/reactor/tests/02_then_bundles/unsafe/005_users.sh b/tests/reactor/tests/02_then_bundles/unsafe/005_users.sh new file mode 100644 index 00000000000..4d9cb5e4a1d --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/unsafe/005_users.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +# check that users promises in a "then" bundle are evaluated on every event + +set -ex + +. "$(dirname "$0")/../../../lib.sh" + +POLICY_FILE="$1" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" + +export TESTFILE EVENTS_FILE + +TEST_USER=$(id -un) +export TEST_USER + +touch "$TESTFILE" + +start_reactor "$POLICY_FILE" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "users kept" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "users kept" From d9988803f6bd81f139b05aa7a757a8d9decb0f49 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Mon, 26 Oct 2026 16:32:51 +0100 Subject: [PATCH 09/10] Fixed undefined classes when running "then" bundles If an event promise is guarded by some class, on reaction to an event, the promise is re-expanded however the class is not defined anymore. Now, the expanded promise is saved into the watcher events. Signed-off-by: Victor Moene --- cf-reactor/reactor_transform.c | 42 +++++++++++++--------------------- cf-reactor/reactor_transform.h | 9 ++++---- cf-reactor/watcher.c | 19 +++++++-------- cf-reactor/watcher.h | 16 ++++++------- libpromises/eval_context.c | 10 ++++++++ libpromises/eval_context.h | 6 +++++ libpromises/policy.c | 31 +++++++++++++++++++++++++ libpromises/policy.h | 8 +++++++ tests/unit/watcher_test.c | 24 ++++++++++--------- 9 files changed, 103 insertions(+), 62 deletions(-) diff --git a/cf-reactor/reactor_transform.c b/cf-reactor/reactor_transform.c index d7a9cc971b9..c3925ffd251 100644 --- a/cf-reactor/reactor_transform.c +++ b/cf-reactor/reactor_transform.c @@ -483,30 +483,14 @@ static PromiseResult RunThenBundles(EvalContext *ctx, const Promise *pp) return result; } -typedef struct -{ - const char *promise_hash; - WatcherCheckResult check; -} ReactorEventParam; - -/* Promise actuator for an events promise whose watcher fired, or failed to - * check for its event. Only acts on the iteration the watcher was registered - * for. */ -static PromiseResult KeepEventsPromiseOnEvent(EvalContext *ctx, const Promise *pp, void *param) +/* Keeps the iteration of an events promise whose watcher fired, or failed to + * check for its event. */ +static PromiseResult KeepEventsPromiseOnEvent(EvalContext *ctx, const Promise *pp, WatcherCheckResult check) { assert(pp != NULL); - assert(param != NULL); - const ReactorEventParam *event = param; - - char promise_hash[CF_HOSTKEY_STRING_SIZE]; - WatcherPromiseHash(pp, promise_hash, sizeof(promise_hash)); - if (!StringEqual(promise_hash, event->promise_hash)) - { - return PROMISE_RESULT_SKIPPED; - } PromiseResult result; - switch (event->check) + switch (check) { case WATCHER_CHECK_EVENT: result = RunThenBundles(ctx, pp); @@ -519,17 +503,16 @@ static PromiseResult KeepEventsPromiseOnEvent(EvalContext *ctx, const Promise *p default: ProgrammingError("Unexpected watcher check result %d for reactor events promise '%s'", - (int) event->check, pp->promiser); + (int) check, pp->promiser); } RecordEventsPromiseOutcome(ctx, pp, result); return result; } -void HandleReactorEvent(EvalContext *ctx, const Promise *pp, const char *promise_hash, WatcherCheckResult check) +void HandleReactorEvent(EvalContext *ctx, const Promise *pp, WatcherCheckResult check) { assert(pp != NULL); - assert(promise_hash != NULL); /* cf-reactor keeps the same EvalContext across events. Like an agent run, * the bundle runs see the time classes of now and the persistent classes @@ -541,11 +524,18 @@ void HandleReactorEvent(EvalContext *ctx, const Promise *pp, const char *promise VariableTable *variables = NULL; EvalContextSnapshotTake(ctx, &classes, &variables); - ReactorEventParam event = { .promise_hash = promise_hash, .check = check }; - + /* pp is the iteration the watcher was registered with, expanded when its + * bundle was evaluated. It is kept as is rather than expanded again, as + * the bundle-local classes its class guard and if/unless were evaluated + * with are gone, and its functions could return something else. */ EvalContextStackPushBundleFrame(ctx, PromiseGetBundle(pp), NULL, false, NULL); EvalContextStackPushBundleSectionFrame(ctx, pp->parent_section); - ExpandPromise(ctx, pp, KeepEventsPromiseOnEvent, &event); + EvalContextStackPushPromiseFrame(ctx, pp); + Promise *pexp = PromiseCopy(pp); + EvalContextStackPushExpandedPromiseIterationFrame(ctx, pexp); + KeepEventsPromiseOnEvent(ctx, pexp, check); + EvalContextStackPopFrame(ctx); /* promise iteration, destroys pexp */ + EvalContextStackPopFrame(ctx); /* promise */ EvalContextStackPopFrame(ctx); /* bundle section */ EvalContextStackPopFrame(ctx); /* bundle */ diff --git a/cf-reactor/reactor_transform.h b/cf-reactor/reactor_transform.h index 01d2ff32bef..bca18f7f2a1 100644 --- a/cf-reactor/reactor_transform.h +++ b/cf-reactor/reactor_transform.h @@ -47,13 +47,12 @@ void KeepReactorPromises(EvalContext *ctx, const Policy *policy); * outcome of the bundle as the outcome of the events promise. If the watcher * failed to check for its event, record a failure instead. * - * @param pp the unexpanded events promise the watcher was registered for - * @param promise_hash the hash of the expanded promise the watcher was - * registered with, selecting the iteration of the events - * promise to keep + * @param pp the expanded events promise the watcher was registered with, + * kept as is: its class guard, if/unless and functions are not + * evaluated again * @param check whether the event happened or the watcher failed to check for * it (see WatcherEventFn) */ -void HandleReactorEvent(EvalContext *ctx, const Promise *pp, const char *promise_hash, WatcherCheckResult check); +void HandleReactorEvent(EvalContext *ctx, const Promise *pp, WatcherCheckResult check); #endif diff --git a/cf-reactor/watcher.c b/cf-reactor/watcher.c index 9d42156cd31..1073f68066c 100644 --- a/cf-reactor/watcher.c +++ b/cf-reactor/watcher.c @@ -51,9 +51,8 @@ typedef struct WatcherCheckFn check_callback; WatcherStateDestroyFn destroy_state; void *state; - const Promise *promise; // the unexpanded events promise, owned by the policy - char *promise_hash; // hash of the expanded events promise, see WatcherPromiseHash() - char *promiser; // expanded promiser of the iteration of `promise` + Promise *promise; // copy of the expanded events promise, see PromiseCopy() + char *promise_hash; // hash of `promise`, see WatcherPromiseHash() time_t poll_interval_secs; time_t next_due; WatcherCheckResult last_check; // only touched by the watcher thread @@ -174,8 +173,7 @@ bool WatcherRegister(const Promise *pp, WatcherOptions *opt) Watcher *w = xmalloc(sizeof(Watcher)); w->promise_hash = xstrdup(promise_hash); - w->promise = pp->org_pp; - w->promiser = xstrdup(pp->promiser); + w->promise = PromiseCopy(pp); w->poll_interval_secs = opt->interval; w->next_due = 0; /* due immediately on the watcher thread's first pass */ w->last_check = WATCHER_CHECK_NO_EVENT; @@ -198,8 +196,8 @@ static void WatcherDestroy(void *item) { w->destroy_state(w->state); } + PromiseDestroy(w->promise); free(w->promise_hash); - free(w->promiser); free(w); } @@ -308,8 +306,7 @@ bool EventWatcherInitialize(int *fd) /* Watchers are only destroyed by the main thread (EventWatcherResume()), * which is also the one handling events, and never while events are queued, * so the watchers of queued events can be used without holding - * watchers_mutex. Their promiser and promise are never modified by the - * watcher thread. */ + * watchers_mutex. Their promise is never modified by the watcher thread. */ static void HandleQueuedEvents(EvalContext *ctx, WatcherEventFn on_event) { assert(on_event != NULL); @@ -321,13 +318,13 @@ static void HandleQueuedEvents(EvalContext *ctx, WatcherEventFn on_event) const Watcher *w = event->watcher; if (event->check == WATCHER_CHECK_ERROR) { - Log(LOG_LEVEL_ERR, "Reactor watcher '%s' failed to check for its event", w->promiser); + Log(LOG_LEVEL_ERR, "Reactor watcher '%s' failed to check for its event", w->promise->promiser); } else { - Log(LOG_LEVEL_VERBOSE, "Reactor watcher '%s' fired", w->promiser); + Log(LOG_LEVEL_VERBOSE, "Reactor watcher '%s' fired", w->promise->promiser); } - on_event(ctx, w->promise, w->promise_hash, event->check); + on_event(ctx, w->promise, event->check); free(event); } } diff --git a/cf-reactor/watcher.h b/cf-reactor/watcher.h index c23768a7a80..bf12a91f63c 100644 --- a/cf-reactor/watcher.h +++ b/cf-reactor/watcher.h @@ -54,10 +54,8 @@ typedef struct * @brief Called on the main thread for every event of a registered watcher, * and whenever a watcher starts failing to check for its event. * - * @param pp the (unexpanded) events promise the watcher was registered for - * @param promise_hash the hash of the expanded promise the watcher was - * registered with (see WatcherPromiseHash()), selecting - * the iteration of the events promise + * @param pp the expanded events promise the watcher was registered with (a + * copy of it, owned by the watcher) * @param check WATCHER_CHECK_EVENT if the event happened, or * WATCHER_CHECK_ERROR if the watcher failed to check for it * (only reported once until a check succeeds again) @@ -65,7 +63,7 @@ typedef struct * EventWatcherPause(), as the events of the watchers are being * handled. */ -typedef void (*WatcherEventFn)(EvalContext *ctx, const Promise *pp, const char *promise_hash, WatcherCheckResult check); +typedef void (*WatcherEventFn)(EvalContext *ctx, const Promise *pp, WatcherCheckResult check); /** * @brief Hash of an expanded events promise, identifying its watcher. @@ -92,10 +90,10 @@ void WatcherRegistryFinalize(void); * reference for the next checks, so that only what happens from now on * fires. If that check fails, the watcher is not registered. * - * @param pp the expanded events promise. Its hash and its unexpanded - * promise (pp->org_pp) are passed back to the WatcherEventFn on - * event. The unexpanded promise is not owned, so the policy - * holding it must not be destroyed before EventWatcherPause(). + * @param pp the expanded events promise. The watcher keeps a copy of it + * (see PromiseCopy()), passed back to the WatcherEventFn on event. + * The copy refers to the policy holding pp, which must not be + * destroyed before EventWatcherPause(). * @param opt what the watcher checks for, see WatcherOptions. opt->state is * owned by the registry, which destroys it right away if not * used, so it must not be used by the caller afterwards. diff --git a/libpromises/eval_context.c b/libpromises/eval_context.c index 944098790f8..0acf39b326f 100644 --- a/libpromises/eval_context.c +++ b/libpromises/eval_context.c @@ -1746,6 +1746,16 @@ Promise *EvalContextStackPushPromiseIterationFrame(EvalContext *ctx, const Promi return pexp; } +void EvalContextStackPushExpandedPromiseIterationFrame(EvalContext *ctx, Promise *pexp) +{ + assert(pexp != NULL); + assert(LastStackFrame(ctx, 0) != NULL); + assert(LastStackFrame(ctx, 0)->type == STACK_FRAME_TYPE_PROMISE); + + EvalContextStackPushFrame(ctx, StackFrameNewPromiseIteration(pexp, NULL)); + LoggingPrivSetLevels(CalculateLogLevel(pexp), CalculateReportLevel(pexp)); +} + void EvalContextStackPopFrame(EvalContext *ctx) { assert(ctx != NULL); diff --git a/libpromises/eval_context.h b/libpromises/eval_context.h index 721c017353b..4b423560889 100644 --- a/libpromises/eval_context.h +++ b/libpromises/eval_context.h @@ -218,6 +218,12 @@ void EvalContextStackPushBodyFrame(EvalContext *ctx, const Promise *caller, cons void EvalContextStackPushBundleSectionFrame(EvalContext *ctx, const BundleSection *owner); void EvalContextStackPushPromiseFrame(EvalContext *ctx, const Promise *owner); Promise *EvalContextStackPushPromiseIterationFrame(EvalContext *ctx, const PromiseIterator *iter_ctx); +/** + * @brief Like EvalContextStackPushPromiseIterationFrame(), for an iteration + * whose promise is already expanded: its constraints, class guard and + * if/unless are not evaluated again. The frame takes ownership of pexp. + */ +void EvalContextStackPushExpandedPromiseIterationFrame(EvalContext *ctx, Promise *pexp); void EvalContextStackPopFrame(EvalContext *ctx); const char *EvalContextStackToString(EvalContext *ctx); void EvalContextSetBundleArgs(EvalContext *ctx, const Rlist *args); diff --git a/libpromises/policy.c b/libpromises/policy.c index 3daae23cf09..083a2b0ec05 100644 --- a/libpromises/policy.c +++ b/libpromises/policy.c @@ -1544,6 +1544,37 @@ static Constraint *ConstraintNew(const char *lval, Rval rval, const char *classe return cp; } +Promise *PromiseCopy(const Promise *pp) +{ + assert(pp != NULL); + + Promise *copy = xcalloc(1, sizeof(Promise)); + copy->parent_section = pp->parent_section; + copy->classes = SafeStringDuplicate(pp->classes); + copy->comment = SafeStringDuplicate(pp->comment); + copy->promiser = SafeStringDuplicate(pp->promiser); + copy->promisee = (pp->promisee.item != NULL) ? + RvalCopy(pp->promisee) : (Rval) { NULL, RVAL_TYPE_NOPROMISEE }; + copy->org_pp = pp->org_pp; + copy->offset = pp->offset; + + if (pp->conlist != NULL) + { + copy->conlist = SeqNew(SeqLength(pp->conlist), ConstraintDestroy); + for (size_t i = 0; i < SeqLength(pp->conlist); i++) + { + const Constraint *cp = SeqAt(pp->conlist, i); + Constraint *cp_copy = ConstraintNew(cp->lval, RvalCopy(cp->rval), cp->classes, cp->references_body); + cp_copy->type = POLICY_ELEMENT_TYPE_PROMISE; + cp_copy->parent.promise = copy; + cp_copy->offset = cp->offset; + SeqAppend(copy->conlist, cp_copy); + } + } + + return copy; +} + Constraint *PromiseAppendConstraint(Promise *pp, const char *lval, Rval rval, bool references_body) { Constraint *cp = ConstraintNew(lval, rval, "any", references_body); diff --git a/libpromises/policy.h b/libpromises/policy.h index 1defbd3a587..39070eb6c10 100644 --- a/libpromises/policy.h +++ b/libpromises/policy.h @@ -206,6 +206,14 @@ void BundleSectionDestroy(BundleSection *section); void PromiseDestroy(Promise *pp); +/** + * @brief Deep copy of a promise and its constraints, e.g. of an expanded + * promise to keep after its iteration. The copy refers to the same + * bundle section and unexpanded promise, so it must not be used once + * the policy is destroyed. + */ +Promise *PromiseCopy(const Promise *pp); + Constraint *PromiseAppendConstraint(Promise *promise, const char *lval, Rval rval, bool references_body); const char *PromiseGetNamespace(const Promise *pp); diff --git a/tests/unit/watcher_test.c b/tests/unit/watcher_test.c index c6fca5155eb..c297793698f 100644 --- a/tests/unit/watcher_test.c +++ b/tests/unit/watcher_test.c @@ -12,9 +12,9 @@ #include /* open() */ /* The registry only hashes the expanded promise it is given (its promiser - * here, as it has no constraints, comment or parent) and hands the - * unexpanded one (org_pp) back to the WatcherEventFn, so minimal promises - * are enough. */ + * here, as it has no constraints, comment or parent) and hands a copy of it + * back to the WatcherEventFn, so minimal promises are enough. Their org_pp + * tells apart the identical promises of different policies. */ static const Promise unexpanded_promise; static const Promise expanded_promise = { .promiser = "test-event", @@ -70,7 +70,7 @@ static bool WaitForQueuedEvent(int fd, int tenths) #define MAX_RECORDED_EVENTS 8 static int handled_event_count = 0; -static const Promise *last_event_promise = NULL; +static const Promise *last_event_org_promise = NULL; static char last_event_promise_hash[CF_HOSTKEY_STRING_SIZE]; static WatcherCheckResult last_event_check = WATCHER_CHECK_NO_EVENT; @@ -78,15 +78,18 @@ static WatcherCheckResult last_event_check = WATCHER_CHECK_NO_EVENT; static char event_promise_hashes[MAX_RECORDED_EVENTS][CF_HOSTKEY_STRING_SIZE]; static void RecordEvent(ARG_UNUSED EvalContext *ctx, const Promise *pp, - const char *promise_hash, WatcherCheckResult check) + WatcherCheckResult check) { + char promise_hash[CF_HOSTKEY_STRING_SIZE]; + WatcherPromiseHash(pp, promise_hash, sizeof(promise_hash)); + if (handled_event_count < MAX_RECORDED_EVENTS) { strlcpy(event_promise_hashes[handled_event_count], promise_hash, sizeof(event_promise_hashes[handled_event_count])); } handled_event_count++; - last_event_promise = pp; + last_event_org_promise = pp->org_pp; strlcpy(last_event_promise_hash, promise_hash, sizeof(last_event_promise_hash)); last_event_check = check; } @@ -96,7 +99,7 @@ static void AssertLastEventFor(const Promise *pp) { char promise_hash[CF_HOSTKEY_STRING_SIZE]; WatcherPromiseHash(pp, promise_hash, sizeof(promise_hash)); - assert_true(last_event_promise == pp->org_pp); + assert_true(last_event_org_promise == pp->org_pp); assert_string_equal(last_event_promise_hash, promise_hash); } @@ -120,7 +123,7 @@ static int RecordedEventCountFor(const Promise *pp) static void ResetRecordedEvents(void) { handled_event_count = 0; - last_event_promise = NULL; + last_event_org_promise = NULL; last_event_promise_hash[0] = '\0'; last_event_check = WATCHER_CHECK_NO_EVENT; for (int i = 0; i < MAX_RECORDED_EVENTS; i++) @@ -274,9 +277,8 @@ static void test_event_watcher_pause_handles_queued_events(void) * second), without handling it */ assert_true(WaitForQueuedEvent(fd, 50)); - /* Pausing handles the queued event, with the unexpanded promise and the - * promise hash of its watcher, so that the watchers can be set aside without - * losing it */ + /* Pausing handles the queued event, with the promise of its watcher, so + * that the watchers can be set aside without losing it */ ResetRecordedEvents(); EventWatcherPause(NULL, RecordEvent); assert_int_equal(handled_event_count, 1); From eada6e3253f4bf638e8e2d9969e6a05ab9bbcb7a Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Tue, 27 Oct 2026 12:04:24 +0100 Subject: [PATCH 10/10] Moved 005_users out of unsafe Since it doesn't actually create users, it doesn't need to be unsafe Signed-off-by: Victor Moene --- tests/reactor/tests/02_then_bundles/{unsafe => }/005_users.cf | 2 +- tests/reactor/tests/02_then_bundles/{unsafe => }/005_users.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename tests/reactor/tests/02_then_bundles/{unsafe => }/005_users.cf (96%) rename tests/reactor/tests/02_then_bundles/{unsafe => }/005_users.sh (95%) diff --git a/tests/reactor/tests/02_then_bundles/unsafe/005_users.cf b/tests/reactor/tests/02_then_bundles/005_users.cf similarity index 96% rename from tests/reactor/tests/02_then_bundles/unsafe/005_users.cf rename to tests/reactor/tests/02_then_bundles/005_users.cf index 1efa2655cbb..bb77ae735e6 100644 --- a/tests/reactor/tests/02_then_bundles/unsafe/005_users.cf +++ b/tests/reactor/tests/02_then_bundles/005_users.cf @@ -5,7 +5,7 @@ ####################################################### body common control { - inputs => { "../../../lib.cf" }; + inputs => { "../../lib.cf" }; bundlesequence => { "test" }; version => "1.0"; } diff --git a/tests/reactor/tests/02_then_bundles/unsafe/005_users.sh b/tests/reactor/tests/02_then_bundles/005_users.sh similarity index 95% rename from tests/reactor/tests/02_then_bundles/unsafe/005_users.sh rename to tests/reactor/tests/02_then_bundles/005_users.sh index 4d9cb5e4a1d..84c8a509110 100644 --- a/tests/reactor/tests/02_then_bundles/unsafe/005_users.sh +++ b/tests/reactor/tests/02_then_bundles/005_users.sh @@ -4,7 +4,7 @@ set -ex -. "$(dirname "$0")/../../../lib.sh" +. "$(dirname "$0")/../../lib.sh" POLICY_FILE="$1"