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/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" 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),