From c1bd1a5918b7989c6fa42d05fc8877340780d61e Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Tue, 20 Oct 2026 15:46:49 +0200 Subject: [PATCH 1/2] Made cf-reactor keep all agent promises Previously cf-reactor only evaluates meta, vars, classes and events promise types. Now, it can run any of the ones avalaible in agent bundles, in addition to events. Event promises and promises found in agent bundles are mutually exclusive inside reactor bundles. When cf-reactor reads the policy to create the watchers, it only does so with bundles that have an events promise Signed-off-by: Victor Moene --- cf-agent/agent_operations.h | 5 +- cf-reactor/README.md | 35 ++++++++++- cf-reactor/reactor_transform.c | 66 +++++++++++-------- cf-reactor/reactor_transform.h | 17 +++-- libpromises/cf3parse_logic.h | 2 +- libpromises/mod_methods.c | 3 +- libpromises/mod_reactor.c | 112 ++++++++++++++++++++++++++++++++- libpromises/mod_reactor.h | 29 +++++++++ libpromises/policy.c | 25 +++++--- libpromises/policy.h | 12 +++- libpromises/syntax.c | 40 ++++++++++++ tests/unit/policy_test.c | 101 +++++++++++++++++++++++++++++ tests/unit/syntax_test.c | 36 +++++++++++ 13 files changed, 434 insertions(+), 49 deletions(-) diff --git a/cf-agent/agent_operations.h b/cf-agent/agent_operations.h index 8604ba69513..42be1fbab35 100644 --- a/cf-agent/agent_operations.h +++ b/cf-agent/agent_operations.h @@ -33,8 +33,9 @@ * EvalContextIsClassicOrder(). * * Used by cf-agent (bundlesequence and methods promises) and cf-reactor - * (bundle in the 'then' attribute of an events promise). Can be called - * recursively through methods promises. + * (bundles in the 'then' attribute of an events promise, including reactor + * bundles without events promises, which have the promise types of agent + * bundles). Can be called recursively through methods promises. * * The caller is responsible for pushing the bundle frame * (EvalContextStackPushBundleFrame()) before calling, and popping it after. diff --git a/cf-reactor/README.md b/cf-reactor/README.md index 0e9c41ad481..b00dafa4e9d 100644 --- a/cf-reactor/README.md +++ b/cf-reactor/README.md @@ -6,6 +6,39 @@ cf-reactor periodically reads the policy to set up the in-memory datastructures needed to react to events. It evaluates reactor bundles and parses events promises to create the corresponding `watchers`. These watchers stay active until the next policy read, when they are rebuilt. +Reactor bundles are of two mutually exclusive kinds: + +- **With events promises**: they can only have `meta`, `vars`, `classes` and `events` promises. On every policy read, each of them without arguments is evaluated in that order, the events promises last, registering the watchers. +- **Without events promises**: they can have every promise type of agent bundles (built-in and custom). They are not evaluated on policy read, they are run from the `then` attribute of events promises, like cf-agent runs agent bundles (`ScheduleAgentOperations()`, in normal or top-down order). + +This is checked when the policy is loaded (see `ReactorBundleCheck()`): having another promise type alongside events promises in a reactor bundle, or referring to a reactor bundle with events promises in a `then` attribute, is an error. + +```cf3 +bundle reactor set_up_events +{ + vars: + "file" string => "/tmp/a"; + + events: + "recreate_a" + when => file_deleted("$(file)"), + then => { create_file("$(file)"), report_recreated }; +} + +bundle reactor create_file(file) +{ + files: + "$(file)" + create => "true"; +} + +bundle reactor report_recreated +{ + reports: + "Recreated the file"; +} +``` + Events promises are parsed into watcher instances, each holding: the event `key` from the events promise, an event `type` (e.g. file deletion), a `state` with whatever state the check needs (e.g. the name of the file being watched), and a `check function` that detects the event by comparing the current state to the state recorded at the last check (e.g. whether the file still exists). It returns `true` only on the specific transition being watched for, so it fires once per change and does not stay `true` afterwards. ### Events @@ -22,7 +55,7 @@ On Linux, we could use inotify instead in the future, and let the kernel poll fo ### Running bundles -On dispatch, the bundle corresponding to an event is run, however it doesn't do a full agent run. +On dispatch, the bundles in the `then` attribute of the events promise are run in order, however it doesn't do a full agent run. They can be agent, common or reactor bundles without events promises (looked up in that order), so a policy for cf-reactor doesn't need agent bundles. ## Implementation details diff --git a/cf-reactor/reactor_transform.c b/cf-reactor/reactor_transform.c index c3925ffd251..4255cbe2b50 100644 --- a/cf-reactor/reactor_transform.c +++ b/cf-reactor/reactor_transform.c @@ -39,25 +39,16 @@ #include // Initialize/FinalizeCustomPromises() #include // SetReferenceTime() #include // UpdateTimeClasses() -#include // REACTOR_INTERVAL_RANGE, REACTOR_FILE_DELETED +#include // REACTOR_INTERVAL_RANGE, REACTOR_FILE_DELETED, REACTOR_*_TYPES, BundleIsReactorEventsBundle() #include // CheckConstraintTypeMatch(), PromiseTypeSyntaxGet(), BodySyntaxGetConstraintSyntax() #include #include -/* Promise types evaluated within `bundle reactor NAME { ... }`. */ -static const char *const REACTOR_TYPESEQUENCE[] = -{ - "meta", - "vars", - "classes", - "events", - NULL -}; - -/* Resolves the agent or common bundle referred to by an entry of an events - * promise's 'then' attribute, either "name" or name(args). If args is not - * NULL, it is set to the bundle arguments (or NULL if there are none). - * Logs an error and returns NULL if the bundle is not found. */ +/* Resolves the agent, common or reactor bundle (looked up in that order) + * referred to by an entry of an events promise's 'then' attribute, either + * "name" or name(args). If args is not NULL, it is set to the bundle + * arguments (or NULL if there are none). Logs an error and returns NULL if + * the bundle is not found, or is a reactor bundle with events promises. */ static const Bundle *ResolveThenBundle( const EvalContext *ctx, const Promise *pp, Rval then_rval, const Rlist **args) { @@ -79,13 +70,10 @@ static const Bundle *ResolveThenBundle( } const Bundle *bp = NULL; - if (name != NULL) + for (size_t i = 0; name != NULL && bp == NULL && REACTOR_THEN_BUNDLE_TYPES[i] != NULL; i++) { - bp = EvalContextResolveBundleExpression(ctx, PromiseGetPolicy(pp), name, "agent"); - if (bp == NULL) - { - bp = EvalContextResolveBundleExpression(ctx, PromiseGetPolicy(pp), name, "common"); - } + bp = EvalContextResolveBundleExpression(ctx, PromiseGetPolicy(pp), name, + REACTOR_THEN_BUNDLE_TYPES[i]); } if (bp == NULL) { @@ -93,6 +81,14 @@ static const Bundle *ResolveThenBundle( pp->promiser, (name != NULL) ? name : "(invalid)"); return NULL; } + if (BundleIsReactorEventsBundle(bp)) + { + Log(LOG_LEVEL_ERR, + "Reactor events promise '%s' cannot run reactor bundle '%s', it has events promises " + "(reactor bundles with events promises are only evaluated when cf-reactor reads the policy)", + pp->promiser, bp->name); + return NULL; + } if (args != NULL) { @@ -368,6 +364,9 @@ static PromiseResult KeepEventsPromise(EvalContext *ctx, const Promise *pp) return PROMISE_RESULT_NOOP; } +/* Promise actuator for the promises of a reactor bundle with events promises + * evaluated on policy read: keeps its meta, vars and classes promises, and + * registers the watcher of each iteration of its events promises. */ static PromiseResult KeepReactorPromise(EvalContext *ctx, const Promise *pp, ARG_UNUSED void *param) { assert(param == NULL); @@ -391,21 +390,31 @@ static PromiseResult KeepReactorPromise(EvalContext *ctx, const Promise *pp, ARG return result; } + /* Only the promise types of REACTOR_EVENTS_BUNDLE_PROMISE_TYPES are + * evaluated, see EvaluateReactorBundle() */ + debug_abort_if_reached(); return PROMISE_RESULT_NOOP; } +/* Keeps the promises of a reactor bundle with events promises on (re)read of + * the policy, in the order of REACTOR_EVENTS_BUNDLE_PROMISE_TYPES: the events + * promises are kept last, so that they see the variables and classes defined + * by the bundle. */ static void EvaluateReactorBundle(EvalContext *ctx, const Bundle *bp) { assert(bp != NULL); + assert(BundleIsReactorEventsBundle(bp)); + + BundleBanner(bp, NULL); EvalContextStackPushBundleFrame(ctx, bp, NULL, false, NULL); - for (int i = 0; REACTOR_TYPESEQUENCE[i] != NULL; i++) + for (int i = 0; REACTOR_EVENTS_BUNDLE_PROMISE_TYPES[i] != NULL; i++) { - const BundleSection *sp = BundleGetSection(bp, REACTOR_TYPESEQUENCE[i]); + const BundleSection *sp = BundleGetSection(bp, REACTOR_EVENTS_BUNDLE_PROMISE_TYPES[i]); if (sp == NULL || SeqLength(sp->promises) == 0) { Log(LOG_LEVEL_DEBUG, "No promise type %s in bundle %s", - REACTOR_TYPESEQUENCE[i], bp->name); + REACTOR_EVENTS_BUNDLE_PROMISE_TYPES[i], bp->name); continue; } @@ -415,10 +424,11 @@ static void EvaluateReactorBundle(EvalContext *ctx, const Bundle *bp) const Promise *pp = SeqAt(sp->promises, j); ExpandPromise(ctx, pp, KeepReactorPromise, NULL); } - EvalContextStackPopFrame(ctx); + EvalContextStackPopFrame(ctx); /* bundle section */ } - EvalContextStackPopFrame(ctx); + EvalContextStackPopFrame(ctx); /* bundle */ + EndBundleBanner(bp); } /* Runs the bundle referred to by an entry of the 'then' attribute of the @@ -549,7 +559,9 @@ void KeepReactorPromises(EvalContext *ctx, const Policy *policy) for (size_t i = 0; i < SeqLength(policy->bundles); i++) { const Bundle *bp = SeqAt(policy->bundles, i); - if (!StringEqual(bp->type, CF_AGENTTYPES[AGENT_TYPE_REACTOR])) + /* The reactor bundles without events promises are only run from the + * 'then' attribute of events promises */ + if (!BundleIsReactorEventsBundle(bp)) { continue; } diff --git a/cf-reactor/reactor_transform.h b/cf-reactor/reactor_transform.h index bca18f7f2a1..3a5da34e00c 100644 --- a/cf-reactor/reactor_transform.h +++ b/cf-reactor/reactor_transform.h @@ -30,9 +30,12 @@ #include /* WatcherCheckResult */ /** - * @brief Evaluate every `bundle reactor NAME { ... }` in the given policy: - * keep its "meta", "vars" and "classes" promises, and register a watcher - * (see watcher.h) for each of its "events" promises. + * @brief Evaluate every `bundle reactor NAME { ... }` with events promises + * and without arguments in the given policy: keep its "meta", "vars" and + * "classes" promises, then register a watcher (see watcher.h) for each of + * its "events" promises. The reactor bundles without events promises are + * not evaluated, they are only run from the 'then' attribute of events + * promises (see BundleIsReactorEventsBundle()). * * Expects no watchers to be registered: on re-read of the policy, the * watchers of the previous policy must be set aside with @@ -43,9 +46,11 @@ void KeepReactorPromises(EvalContext *ctx, const Policy *policy); /** * @brief Keep the events promise of a watcher that fired (see WatcherEventFn - * in watcher.h): run the bundle of its 'then' attribute, and record the - * outcome of the bundle as the outcome of the events promise. If the watcher - * failed to check for its event, record a failure instead. + * in watcher.h): run the agent, common or reactor bundles (without events + * promises) of its 'then' attribute like cf-agent does (see + * ScheduleAgentOperations()), and record the outcome of the bundles as the + * outcome of the events promise. If the watcher failed to check for its + * event, record a failure instead. * * @param pp the expanded events promise the watcher was registered with, * kept as is: its class guard, if/unless and functions are not diff --git a/libpromises/cf3parse_logic.h b/libpromises/cf3parse_logic.h index 56a71005f24..220de9ed15b 100644 --- a/libpromises/cf3parse_logic.h +++ b/libpromises/cf3parse_logic.h @@ -231,7 +231,7 @@ static bool RelevantBundle(const char *agent, const char *blocktype) } /* Here are some additional bundle types handled by cfAgent, and by - * cf-reactor for the agent bundles it runs */ + * cf-reactor for the agent and reactor bundles it runs */ Item *ip = SplitString("edit_line,edit_xml", ','); diff --git a/libpromises/mod_methods.c b/libpromises/mod_methods.c index a9716bda3cb..6133a1c2868 100644 --- a/libpromises/mod_methods.c +++ b/libpromises/mod_methods.c @@ -37,7 +37,8 @@ static const ConstraintSyntax CF_METHOD_BODIES[] = static bool MethodsParseTreeCheck(const Promise *pp, Seq *errors) { - return PromiseCheckBundleCallArity(pp, "usebundle", errors); + static const char *const bundle_types[] = { CF_AGENTC, CF_COMMONC, NULL }; + return PromiseCheckBundleCallArity(pp, "usebundle", bundle_types, errors); } const PromiseTypeSyntax CF_METHOD_PROMISE_TYPES[] = diff --git a/libpromises/mod_reactor.c b/libpromises/mod_reactor.c index b65761c42cf..af4a1fd82a3 100644 --- a/libpromises/mod_reactor.c +++ b/libpromises/mod_reactor.c @@ -27,7 +27,9 @@ #include #include #include -#include +#include +#include /* ClassRefParse() */ +#include /* StringEqual(), IsStrIn() */ static const char *const POLICY_ERROR_EVENTS_MISSING_ATTRIBUTE = "Missing required attribute '%s' for promise type 'events'"; @@ -78,10 +80,103 @@ static const BodySyntax when_body = BodySyntaxNew("when", when_constraints, &Whe static const ConstraintSyntax CF_EVENT_BODIES[] = { ConstraintSyntaxNewBody("when", &when_body, "Event to react to", SYNTAX_STATUS_NORMAL), - ConstraintSyntaxNewBundleList("then", "Bundle or list of bundles to run in order on event", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewBundleList("then", "Agent, common or reactor bundle (without events promises) or list of bundles to run in order on event", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewNull() }; +const char *const REACTOR_THEN_BUNDLE_TYPES[] = { CF_AGENTC, CF_COMMONC, CF_REACTORC, NULL }; + +const char *const REACTOR_EVENTS_BUNDLE_PROMISE_TYPES[] = { "meta", "vars", "classes", "events", NULL }; + +bool BundleIsReactorEventsBundle(const Bundle *bp) +{ + assert(bp != NULL); + return StringEqual(bp->type, CF_REACTORC) && (BundleGetSection(bp, "events") != NULL); +} + +static bool ReactorEventsBundleCheckSections(const Seq *sections, Seq *errors) +{ + bool success = true; + for (size_t i = 0; i < SeqLength(sections); i++) + { + const BundleSection *sp = SeqAt(sections, i); + if (!IsStrIn(sp->promise_type, REACTOR_EVENTS_BUNDLE_PROMISE_TYPES)) + { + SeqAppend(errors, PolicyErrorNew(POLICY_ELEMENT_TYPE_BUNDLE_SECTION, sp, + "Promise type '%s' not supported by reactor bundles with events promises, " + "which can only have meta, vars, classes and events promises " + "(the other promise types go in reactor bundles without events promises, " + "run from the 'then' attribute of events promises)", + sp->promise_type)); + success = false; + } + } + return success; +} + +bool ReactorBundleCheck(const Bundle *bp, Seq *errors) +{ + assert(bp != NULL); + assert(StringEqual(bp->type, CF_REACTORC)); + + if (!BundleIsReactorEventsBundle(bp)) + { + return true; + } + + bool success = true; + success &= ReactorEventsBundleCheckSections(bp->sections, errors); + success &= ReactorEventsBundleCheckSections(bp->custom_sections, errors); + return success; +} + +/* Checks that an entry of the 'then' attribute of the events promise, either + * "name" or name(args), does not refer to a reactor bundle with events + * promises, if the bundle is found. */ +static bool ThenBundleCheck(const Promise *pp, const Constraint *cp, Rval rval, Seq *errors) +{ + assert(pp != NULL); + assert(cp != NULL); + + const char *name = NULL; + switch (rval.type) + { + case RVAL_TYPE_SCALAR: + name = RvalScalarValue(rval); + break; + case RVAL_TYPE_FNCALL: + name = RvalFnCallValue(rval)->name; + break; + default: + return true; + } + + // HACK: exploiting the fact that class-references and call-references are similar + ClassRef ref = ClassRefParse(name); + if (!ClassRefIsQualified(ref)) + { + ClassRefQualify(&ref, PromiseGetNamespace(pp)); + } + + const Bundle *callee = NULL; + for (size_t i = 0; callee == NULL && REACTOR_THEN_BUNDLE_TYPES[i] != NULL; i++) + { + callee = PolicyGetBundle(PromiseGetPolicy(pp), ref.ns, REACTOR_THEN_BUNDLE_TYPES[i], ref.name); + } + + ClassRefDestroy(ref); + + if (callee != NULL && BundleIsReactorEventsBundle(callee)) + { + SeqAppend(errors, PolicyErrorNew(POLICY_ELEMENT_TYPE_CONSTRAINT, cp, + "Events promise cannot run reactor bundle '%s', it has events promises " + "(reactor bundles with events promises are only evaluated when cf-reactor reads the policy)", + name)); + return false; + } + return true; +} + static bool EventsParseTreeCheck(const Promise *pp, Seq *errors) { bool success = true; @@ -108,8 +203,19 @@ static bool EventsParseTreeCheck(const Promise *pp, Seq *errors) POLICY_ERROR_EVENTS_EMPTY_THEN)); success = false; } + else if (then->rval.type == RVAL_TYPE_LIST) + { + for (const Rlist *rp = RvalRlistValue(then->rval); rp != NULL; rp = rp->next) + { + success &= ThenBundleCheck(pp, then, rp->val, errors); + } + } + else + { + success &= ThenBundleCheck(pp, then, then->rval, errors); + } - success &= PromiseCheckBundleCallArity(pp, "then", errors); + success &= PromiseCheckBundleCallArity(pp, "then", REACTOR_THEN_BUNDLE_TYPES, errors); return success; } diff --git a/libpromises/mod_reactor.h b/libpromises/mod_reactor.h index d84ed22643c..64560e7a07c 100644 --- a/libpromises/mod_reactor.h +++ b/libpromises/mod_reactor.h @@ -26,12 +26,41 @@ #define CFENGINE_MOD_REACTOR_H #include +#include #define REACTOR_FILE_DELETED "file_deleted" /* Range of the 'interval' attribute of the 'when' body of events promises */ #define REACTOR_INTERVAL_RANGE "1,99999999999" +/* Types of the bundles the 'then' attribute of events promises can refer to, + * in order of lookup, terminated by NULL */ +extern const char *const REACTOR_THEN_BUNDLE_TYPES[]; + +/* Promise types of the reactor bundles with events promises, in order of + * evaluation, terminated by NULL */ +extern const char *const REACTOR_EVENTS_BUNDLE_PROMISE_TYPES[]; + +/** + * @brief Whether the bundle is a reactor bundle with events promises. + * + * Reactor bundles are of two mutually exclusive kinds: + * - with events promises, they can only have the promise types of + * REACTOR_EVENTS_BUNDLE_PROMISE_TYPES. cf-reactor evaluates them when it + * reads the policy, registering the watchers of their events promises. + * - without events promises, they can have every promise type of agent + * bundles. cf-reactor runs them like cf-agent runs agent bundles, from the + * 'then' attribute of events promises. + */ +bool BundleIsReactorEventsBundle(const Bundle *bp); + +/** + * @brief Checks that a reactor bundle with events promises has no other + * promise types than those of REACTOR_EVENTS_BUNDLE_PROMISE_TYPES. + * @return false if it has, appending the errors to errors, true otherwise + */ +bool ReactorBundleCheck(const Bundle *bp, Seq *errors); + extern const PromiseTypeSyntax CF_REACTOR_PROMISE_TYPES[]; #endif diff --git a/libpromises/policy.c b/libpromises/policy.c index 083a2b0ec05..1c24bca098d 100644 --- a/libpromises/policy.c +++ b/libpromises/policy.c @@ -46,6 +46,7 @@ #include #include /* FileLock */ #include /* GetStateDir() */ +#include /* ReactorBundleCheck() */ static const char *const POLICY_ERROR_BUNDLE_NAME_RESERVED = "Use of a reserved container name as a bundle name \"%s\""; @@ -732,6 +733,11 @@ static bool PolicyCheckBundle(const Bundle *bundle, Seq *errors) success &= PolicyCheckBundleSections(bundle->sections, errors); success &= PolicyCheckBundleSections(bundle->custom_sections, errors); + if (StringEqual(bundle->type, CF_REACTORC)) + { + success &= ReactorBundleCheck(bundle, errors); + } + return success; } @@ -2762,8 +2768,11 @@ static bool ValidateCustomPromise(const Promise *pp, Seq *errors) return valid; } -static bool BundleCallCheckArity(const Promise *pp, const Constraint *cp, Rval rval, Seq *errors) +static bool BundleCallCheckArity(const Promise *pp, const Constraint *cp, Rval rval, + const char *const bundle_types[], Seq *errors) { + assert(bundle_types != NULL); + if (rval.type != RVAL_TYPE_FNCALL) { return true; @@ -2777,10 +2786,10 @@ static bool BundleCallCheckArity(const Promise *pp, const Constraint *cp, Rval r ClassRefQualify(&ref, PromiseGetNamespace(pp)); } - const Bundle *callee = PolicyGetBundle(PolicyFromPromise(pp), ref.ns, "agent", ref.name); - if (!callee) + const Bundle *callee = NULL; + for (size_t i = 0; callee == NULL && bundle_types[i] != NULL; i++) { - callee = PolicyGetBundle(PolicyFromPromise(pp), ref.ns, "common", ref.name); + callee = PolicyGetBundle(PolicyFromPromise(pp), ref.ns, bundle_types[i], ref.name); } ClassRefDestroy(ref); @@ -2798,10 +2807,12 @@ static bool BundleCallCheckArity(const Promise *pp, const Constraint *cp, Rval r return true; } -bool PromiseCheckBundleCallArity(const Promise *pp, const char *lval, Seq *errors) +bool PromiseCheckBundleCallArity(const Promise *pp, const char *lval, + const char *const bundle_types[], Seq *errors) { assert(pp != NULL); assert(lval != NULL); + assert(bundle_types != NULL); bool success = true; @@ -2816,12 +2827,12 @@ bool PromiseCheckBundleCallArity(const Promise *pp, const char *lval, Seq *error { for (const Rlist *rp = RvalRlistValue(cp->rval); rp != NULL; rp = rp->next) { - success &= BundleCallCheckArity(pp, cp, rp->val, errors); + success &= BundleCallCheckArity(pp, cp, rp->val, bundle_types, errors); } } else { - success &= BundleCallCheckArity(pp, cp, cp->rval, errors); + success &= BundleCallCheckArity(pp, cp, cp->rval, bundle_types, errors); } } } diff --git a/libpromises/policy.h b/libpromises/policy.h index 39070eb6c10..551057289f5 100644 --- a/libpromises/policy.h +++ b/libpromises/policy.h @@ -220,7 +220,17 @@ const char *PromiseGetNamespace(const Promise *pp); const Bundle *PromiseGetBundle(const Promise *pp); const Policy *PromiseGetPolicy(const Promise *pp); -bool PromiseCheckBundleCallArity(const Promise *pp, const char *lval, Seq *errors); +/** + * @brief Checks that the bundle calls of the lval attribute of the promise + * (a bundle or a list of bundles) pass as many arguments as the called + * bundles take. + * @param bundle_types The types of bundle the calls can refer to, in order of + * lookup, terminated by NULL + * @return false if a call has a wrong number of arguments, appending the + * error to errors, true otherwise + */ +bool PromiseCheckBundleCallArity(const Promise *pp, const char *lval, + const char *const bundle_types[], Seq *errors); static inline const char *PromiseGetPromiseType(const Promise *pp) { diff --git a/libpromises/syntax.c b/libpromises/syntax.c index 0d15ff7e819..57686c5215a 100644 --- a/libpromises/syntax.c +++ b/libpromises/syntax.c @@ -93,9 +93,29 @@ bool IsBuiltInPromiseType(const char *const promise_type) return false; } +/* Returns the bundle type whose promise types bundles of the given type can + * also have, or NULL. Reactor bundles can have every promise type of agent + * bundles, as cf-reactor evaluates them like cf-agent does, in addition to + * their own (events promises). */ +static const char *BundleTypeGetInheritedType(const char *bundle_type) +{ + assert(bundle_type != NULL); + + if (StringEqual(bundle_type, CF_REACTORC)) + { + return CF_AGENTC; + } + return NULL; +} + const PromiseTypeSyntax *PromiseTypeSyntaxGet(const char *bundle_type, const char *promise_type) { const PromiseTypeSyntax *pts = PromiseTypeSyntaxGetStrict(bundle_type, promise_type); + const char *inherited_type = BundleTypeGetInheritedType(bundle_type); + if (!pts && inherited_type != NULL) + { + pts = PromiseTypeSyntaxGetStrict(inherited_type, promise_type); + } if (!pts) { pts = PromiseTypeSyntaxGetStrict("*", promise_type); @@ -1116,9 +1136,29 @@ static JsonElement *BundleTypesToJson(void) JsonObjectAppendObject(bundle_types, "common", bundle_type); } + // Append the promise types of the bundle types they inherit from, before + // the common promise types are appended to them JsonIterator it = JsonIteratorInit(bundle_types); const char *bundle_type = NULL; while ((bundle_type = JsonIteratorNextKey(&it))) + { + const char *inherited_type = BundleTypeGetInheritedType(bundle_type); + if (inherited_type == NULL) + { + continue; + } + + JsonElement *promise_types = JsonObjectGetAsArray(JsonObjectGetAsObject(bundle_types, bundle_type), "promiseTypes"); + JsonElement *inherited_promise_types = JsonObjectGetAsArray(JsonObjectGetAsObject(bundle_types, inherited_type), "promiseTypes"); + assert(inherited_promise_types != NULL); + for (size_t i = 0; i < JsonLength(inherited_promise_types); i++) + { + JsonArrayAppendString(promise_types, JsonArrayGetAsString(inherited_promise_types, i)); + } + } + + it = JsonIteratorInit(bundle_types); + while ((bundle_type = JsonIteratorNextKey(&it))) { JsonElement *promise_types = JsonObjectGetAsArray(JsonObjectGetAsObject(bundle_types, bundle_type), "promiseTypes"); for (size_t i = 0; i < SeqLength(common_promise_types); i++) diff --git a/tests/unit/policy_test.c b/tests/unit/policy_test.c index 6d6db41a170..1c66994832d 100644 --- a/tests/unit/policy_test.c +++ b/tests/unit/policy_test.c @@ -455,6 +455,103 @@ static void test_body_action_with_log_repaired_needs_log_string(void) } } +#define REACTOR_WHEN_BODY "body when deleted(file) { file_deleted => '$(file)'; }" + +static void test_reactor_bundle_with_events(void) +{ + { + /* Only meta, vars, classes and events promises */ + Seq *errs = LoadAndCheckString(REACTOR_WHEN_BODY + "bundle reactor foo {" + " meta: 'tags' slist => { 'a' };" + " vars: 'f' string => '/tmp/a';" + " classes: 'c' expression => 'any';" + " events: 'e' when => deleted('$(f)'), then => 'bar';" + "}" + "bundle reactor bar {" + " reports: 'deleted';" + "}"); + assert_int_equal(0, errs->length); + SeqDestroy(errs); + } + + { + /* A built-in promise type of agent bundles */ + Seq *errs = LoadAndCheckString(REACTOR_WHEN_BODY + "bundle reactor foo {" + " files: '/tmp/a' create => 'true';" + " events: 'e' when => deleted('/tmp/a'), then => 'bar';" + "}" + "bundle agent bar {" + " reports: 'deleted';" + "}"); + assert_int_equal(1, errs->length); + SeqDestroy(errs); + } + + { + /* A custom promise type */ + Seq *errs = LoadAndCheckString(REACTOR_WHEN_BODY + "bundle reactor foo {" + " git: '/tmp/a' repository => 'x';" + " events: 'e' when => deleted('/tmp/a'), then => 'bar';" + "}" + "bundle agent bar {" + " reports: 'deleted';" + "}"); + assert_int_equal(1, errs->length); + SeqDestroy(errs); + } +} + +static void test_reactor_bundle_without_events(void) +{ + /* The promise types of agent bundles */ + Seq *errs = LoadAndCheckString("bundle reactor foo(file) {" + " vars: 'f' string => '$(file)';" + " files: '$(f)' create => 'true';" + " methods: 'm' usebundle => bar;" + " reports: 'created';" + "}" + "bundle agent bar {" + " reports: 'bar';" + "}"); + assert_int_equal(0, errs->length); + SeqDestroy(errs); +} + +static void test_reactor_then_bundle_with_events(void) +{ + { + Seq *errs = LoadAndCheckString(REACTOR_WHEN_BODY + "bundle reactor foo {" + " events: 'e' when => deleted('/tmp/a'), then => { 'bar', baz('x') };" + "}" + "bundle reactor bar {" + " events: 'e' when => deleted('/tmp/b'), then => 'foo';" + "}" + "bundle reactor baz(x) {" + " events: 'e' when => deleted('$(x)'), then => 'foo';" + "}"); + /* bar and baz refer to foo, foo refers to bar and baz */ + assert_int_equal(4, errs->length); + SeqDestroy(errs); + } + + { + /* Agent bundles are looked up before reactor bundles */ + Seq *errs = LoadAndCheckString(REACTOR_WHEN_BODY + "bundle reactor foo {" + " events: 'e' when => deleted('/tmp/a'), then => 'foo';" + "}" + "bundle agent foo {" + " reports: 'deleted';" + "}"); + assert_int_equal(0, errs->length); + SeqDestroy(errs); + } +} + int main() { PRINT_TEST_BANNER(); @@ -481,6 +578,10 @@ int main() unit_test(test_promiser_empty_varref), unit_test(test_body_action_with_log_repaired_needs_log_string), + + unit_test(test_reactor_bundle_with_events), + unit_test(test_reactor_bundle_without_events), + unit_test(test_reactor_then_bundle_with_events), }; return run_tests(tests); diff --git a/tests/unit/syntax_test.c b/tests/unit/syntax_test.c index 69b9062b59e..92ab3ebcfbc 100644 --- a/tests/unit/syntax_test.c +++ b/tests/unit/syntax_test.c @@ -17,6 +17,38 @@ static void test_lookup_promise_type_common_vars(void) assert_string_equal("vars", s->promise_type); } +static void test_lookup_promise_type_reactor_events(void) +{ + const PromiseTypeSyntax *s = PromiseTypeSyntaxGet("reactor", "events"); + assert_true(s); + assert_string_equal("reactor", s->bundle_type); + assert_string_equal("events", s->promise_type); +} + +static void test_lookup_promise_type_reactor_files(void) +{ + /* Reactor bundles have the promise types of agent bundles */ + const PromiseTypeSyntax *s = PromiseTypeSyntaxGet("reactor", "files"); + assert_true(s); + assert_true(s == PromiseTypeSyntaxGet("agent", "files")); + assert_string_equal("agent", s->bundle_type); + assert_string_equal("files", s->promise_type); +} + +static void test_lookup_promise_type_reactor_vars(void) +{ + const PromiseTypeSyntax *s = PromiseTypeSyntaxGet("reactor", "vars"); + assert_true(s); + assert_string_equal("vars", s->promise_type); +} + +static void test_lookup_promise_type_not_inherited(void) +{ + /* Only reactor bundles have the promise types of agent bundles */ + assert_true(PromiseTypeSyntaxGet("common", "files") == NULL); + assert_true(PromiseTypeSyntaxGet("agent", "events") == NULL); +} + static void test_lookup_promise_type_edit_xml_build_xpath(void) { const PromiseTypeSyntax *s = PromiseTypeSyntaxGet("edit_xml", "build_xpath"); @@ -164,6 +196,10 @@ int main() { unit_test(test_lookup_promise_type_agent_vars), unit_test(test_lookup_promise_type_common_vars), + unit_test(test_lookup_promise_type_reactor_events), + unit_test(test_lookup_promise_type_reactor_files), + unit_test(test_lookup_promise_type_reactor_vars), + unit_test(test_lookup_promise_type_not_inherited), unit_test(test_lookup_promise_type_edit_xml_build_xpath), unit_test(test_lookup_promise_type_edit_line_delete_lines), From ad36a1fce42272b65812730a70a56ef545c3d051 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Tue, 27 Oct 2026 11:54:31 +0100 Subject: [PATCH 2/2] Updated tests Instead of running agent bundles inside "then" attribute, we can run reactor bundles Signed-off-by: Victor Moene --- .../reactor/tests/02_then_bundles/001_meta.cf | 2 +- .../reactor/tests/02_then_bundles/002_vars.cf | 2 +- .../tests/02_then_bundles/003_defaults.cf | 2 +- .../tests/02_then_bundles/004_classes.cf | 2 +- .../tests/02_then_bundles/005_users.cf | 2 +- .../tests/02_then_bundles/006_files.cf | 2 +- .../tests/02_then_bundles/007_packages.cf | 2 +- .../02_then_bundles/008_guest_environments.cf | 2 +- .../tests/02_then_bundles/009_methods.cf | 2 +- .../tests/02_then_bundles/010_processes.cf | 2 +- .../tests/02_then_bundles/011_services.cf | 2 +- .../tests/02_then_bundles/012_commands.cf | 2 +- .../tests/02_then_bundles/013_storage.cf | 2 +- .../tests/02_then_bundles/014_databases.cf | 2 +- .../tests/02_then_bundles/015_reports.cf | 2 +- .../tests/02_then_bundles/016_custom.cf | 2 +- .../tests/02_then_bundles/017_agent_bundle.cf | 41 +++++++++++++++++++ .../tests/02_then_bundles/017_agent_bundle.sh | 39 ++++++++++++++++++ 18 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 tests/reactor/tests/02_then_bundles/017_agent_bundle.cf create mode 100644 tests/reactor/tests/02_then_bundles/017_agent_bundle.sh diff --git a/tests/reactor/tests/02_then_bundles/001_meta.cf b/tests/reactor/tests/02_then_bundles/001_meta.cf index e43111b40e6..b1b1a91c4ec 100644 --- a/tests/reactor/tests/02_then_bundles/001_meta.cf +++ b/tests/reactor/tests/02_then_bundles/001_meta.cf @@ -17,7 +17,7 @@ body when 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) +bundle reactor run_meta_promises(events_file, value_file) { meta: "value" string => readfile("$(value_file)"); diff --git a/tests/reactor/tests/02_then_bundles/002_vars.cf b/tests/reactor/tests/02_then_bundles/002_vars.cf index e2aefbe08c4..be5d9a9f26b 100644 --- a/tests/reactor/tests/02_then_bundles/002_vars.cf +++ b/tests/reactor/tests/02_then_bundles/002_vars.cf @@ -15,7 +15,7 @@ body when deleted(file) file_deleted => "$(file)"; } -bundle agent run_vars(events_file, value_file) +bundle reactor run_vars(events_file, value_file) { vars: # execresult() is a cached function: the cache must not outlive an event diff --git a/tests/reactor/tests/02_then_bundles/003_defaults.cf b/tests/reactor/tests/02_then_bundles/003_defaults.cf index 0f604a5d507..4116faa9785 100644 --- a/tests/reactor/tests/02_then_bundles/003_defaults.cf +++ b/tests/reactor/tests/02_then_bundles/003_defaults.cf @@ -15,7 +15,7 @@ body when deleted(file) file_deleted => "$(file)"; } -bundle agent run_defaults(events_file, msg) +bundle reactor run_defaults(events_file, msg) { defaults: "msg" diff --git a/tests/reactor/tests/02_then_bundles/004_classes.cf b/tests/reactor/tests/02_then_bundles/004_classes.cf index 0b7c32a8982..0a7f40e4742 100644 --- a/tests/reactor/tests/02_then_bundles/004_classes.cf +++ b/tests/reactor/tests/02_then_bundles/004_classes.cf @@ -15,7 +15,7 @@ body when deleted(file) file_deleted => "$(file)"; } -bundle agent run_classes(events_file, marker) +bundle reactor run_classes(events_file, marker) { classes: # returnszero() is a cached function: the cache must not outlive an event diff --git a/tests/reactor/tests/02_then_bundles/005_users.cf b/tests/reactor/tests/02_then_bundles/005_users.cf index bb77ae735e6..29bbb8063fd 100644 --- a/tests/reactor/tests/02_then_bundles/005_users.cf +++ b/tests/reactor/tests/02_then_bundles/005_users.cf @@ -15,7 +15,7 @@ body when deleted(file) file_deleted => "$(file)"; } -bundle agent run_users(user) +bundle reactor run_users(user) { users: # the user running the test, so that it is kept without being root diff --git a/tests/reactor/tests/02_then_bundles/006_files.cf b/tests/reactor/tests/02_then_bundles/006_files.cf index 90a482eee6f..66a06505920 100644 --- a/tests/reactor/tests/02_then_bundles/006_files.cf +++ b/tests/reactor/tests/02_then_bundles/006_files.cf @@ -15,7 +15,7 @@ body when deleted(file) file_deleted => "$(file)"; } -bundle agent run_files(file, line) +bundle reactor run_files(file, line) { files: "$(file)" diff --git a/tests/reactor/tests/02_then_bundles/007_packages.cf b/tests/reactor/tests/02_then_bundles/007_packages.cf index 84bcd32de98..1820aef992f 100644 --- a/tests/reactor/tests/02_then_bundles/007_packages.cf +++ b/tests/reactor/tests/02_then_bundles/007_packages.cf @@ -24,7 +24,7 @@ body package_module reactor_test default_options => { "$(test.installed)" }; } -bundle agent run_packages +bundle reactor run_packages { packages: "reactor_test_pkg" diff --git a/tests/reactor/tests/02_then_bundles/008_guest_environments.cf b/tests/reactor/tests/02_then_bundles/008_guest_environments.cf index 1297334a885..9cbb424c202 100644 --- a/tests/reactor/tests/02_then_bundles/008_guest_environments.cf +++ b/tests/reactor/tests/02_then_bundles/008_guest_environments.cf @@ -15,7 +15,7 @@ body when deleted(file) file_deleted => "$(file)"; } -bundle agent run_guest_environments +bundle reactor run_guest_environments { guest_environments: # the guest that the "test" hypervisor of libvirt (test:///default) runs. diff --git a/tests/reactor/tests/02_then_bundles/009_methods.cf b/tests/reactor/tests/02_then_bundles/009_methods.cf index ab373e2a142..9ecf147cbd7 100644 --- a/tests/reactor/tests/02_then_bundles/009_methods.cf +++ b/tests/reactor/tests/02_then_bundles/009_methods.cf @@ -15,7 +15,7 @@ body when deleted(file) file_deleted => "$(file)"; } -bundle agent run_methods(events_file, value_file) +bundle reactor run_methods(events_file, value_file) { methods: "any" diff --git a/tests/reactor/tests/02_then_bundles/010_processes.cf b/tests/reactor/tests/02_then_bundles/010_processes.cf index f25585e625b..23c0d3ce350 100644 --- a/tests/reactor/tests/02_then_bundles/010_processes.cf +++ b/tests/reactor/tests/02_then_bundles/010_processes.cf @@ -15,7 +15,7 @@ body when deleted(file) file_deleted => "$(file)"; } -bundle agent run_processes +bundle reactor run_processes { processes: "sleep 978654" diff --git a/tests/reactor/tests/02_then_bundles/011_services.cf b/tests/reactor/tests/02_then_bundles/011_services.cf index 900f05466a6..00ebf7b8482 100644 --- a/tests/reactor/tests/02_then_bundles/011_services.cf +++ b/tests/reactor/tests/02_then_bundles/011_services.cf @@ -22,7 +22,7 @@ body service_method record_service(events_file, service) ); } -bundle agent run_services(events_file) +bundle reactor run_services(events_file) { services: "reactor_test_service" diff --git a/tests/reactor/tests/02_then_bundles/012_commands.cf b/tests/reactor/tests/02_then_bundles/012_commands.cf index 55579d73934..80a05f1a5c5 100644 --- a/tests/reactor/tests/02_then_bundles/012_commands.cf +++ b/tests/reactor/tests/02_then_bundles/012_commands.cf @@ -20,7 +20,7 @@ body contain in_shell useshell => "useshell"; } -bundle agent run_commands(events_file, output, module_file) +bundle reactor run_commands(events_file, output, module_file) { commands: "echo ran >> '$(output)'" diff --git a/tests/reactor/tests/02_then_bundles/013_storage.cf b/tests/reactor/tests/02_then_bundles/013_storage.cf index 9968a0e9c14..f78f320bab0 100644 --- a/tests/reactor/tests/02_then_bundles/013_storage.cf +++ b/tests/reactor/tests/02_then_bundles/013_storage.cf @@ -20,7 +20,7 @@ body volume any_free_space freespace => "1k"; } -bundle agent run_storage +bundle reactor run_storage { storage: "$(sys.workdir)" diff --git a/tests/reactor/tests/02_then_bundles/014_databases.cf b/tests/reactor/tests/02_then_bundles/014_databases.cf index 881c8cda5e2..6b9590d17c0 100644 --- a/tests/reactor/tests/02_then_bundles/014_databases.cf +++ b/tests/reactor/tests/02_then_bundles/014_databases.cf @@ -26,7 +26,7 @@ body database_server no_server db_server_connection_db => "postgres"; } -bundle agent run_databases +bundle reactor run_databases { databases: "reactor_test" diff --git a/tests/reactor/tests/02_then_bundles/015_reports.cf b/tests/reactor/tests/02_then_bundles/015_reports.cf index 7d962d2129e..5b83f855a32 100644 --- a/tests/reactor/tests/02_then_bundles/015_reports.cf +++ b/tests/reactor/tests/02_then_bundles/015_reports.cf @@ -15,7 +15,7 @@ body when deleted(file) file_deleted => "$(file)"; } -bundle agent run_reports(events_file, value_file) +bundle reactor run_reports(events_file, value_file) { vars: "value" string => readfile("$(value_file)"); diff --git a/tests/reactor/tests/02_then_bundles/016_custom.cf b/tests/reactor/tests/02_then_bundles/016_custom.cf index 7238fd37a83..0f923afecaa 100644 --- a/tests/reactor/tests/02_then_bundles/016_custom.cf +++ b/tests/reactor/tests/02_then_bundles/016_custom.cf @@ -21,7 +21,7 @@ promise agent reactor_test path => "$(sys.workdir)/modules/promises/reactor_test.sh"; } -bundle agent run_custom(output) +bundle reactor run_custom(output) { reactor_test: "$(output)" diff --git a/tests/reactor/tests/02_then_bundles/017_agent_bundle.cf b/tests/reactor/tests/02_then_bundles/017_agent_bundle.cf new file mode 100644 index 00000000000..bf018ca8972 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/017_agent_bundle.cf @@ -0,0 +1,41 @@ +####################################################### +# +# Test an agent bundle as a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_agent_bundle(events_file, value_file) +{ + vars: + "value" string => readfile("$(value_file)"); + + reports: + "$(with): agent $(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_agent_bundle("$(events_file)", "$(value_file)"); +} diff --git a/tests/reactor/tests/02_then_bundles/017_agent_bundle.sh b/tests/reactor/tests/02_then_bundles/017_agent_bundle.sh new file mode 100644 index 00000000000..41c5adfe169 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/017_agent_bundle.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +# check that an agent bundle can be a "then" bundle, like a reactor bundle +# without events promises: its promises 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 "agent 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 "agent second"