From e39a47d67c46a0a85190a05013b1d1ae59f61de5 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Thu, 8 Oct 2026 13:46:57 +0200 Subject: [PATCH] Accept list of bundles in "then" attribute in event promises The "then" attribute in events promises accepts both simple bundles and list of bundles, similar to bundlsequences. Ticket: ENT-14197 Signed-off-by: Victor Moene --- cf-reactor/reactor_transform.c | 96 ++++++++++++++++++++++++---------- libpromises/cf3.defs.h | 1 + libpromises/cf3parse_logic.h | 3 +- libpromises/conversion.c | 1 + libpromises/expand.c | 33 ++++++++++++ libpromises/expand.h | 1 + libpromises/mod_reactor.c | 2 +- libpromises/policy.c | 69 +++++++++++++++--------- libpromises/promises.c | 8 ++- libpromises/rlist.c | 1 + libpromises/syntax.c | 17 ++++-- libpromises/syntax.h | 1 + 12 files changed, 173 insertions(+), 60 deletions(-) diff --git a/cf-reactor/reactor_transform.c b/cf-reactor/reactor_transform.c index ad909c84994..27c725a46fb 100644 --- a/cf-reactor/reactor_transform.c +++ b/cf-reactor/reactor_transform.c @@ -51,9 +51,9 @@ static const char *const REACTOR_TYPESEQUENCE[] = NULL }; -/* Resolves the agent or common bundle referred to by an events promise's - * 'then' attribute, either `then => "name"` or `then => name(args)`. If args is - * not NULL, it is set to the bundle arguments (or NULL if there are none). +/* 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. */ static const Bundle *ResolveThenBundle( const EvalContext *ctx, const Promise *pp, Rval then_rval, const Rlist **args) @@ -110,10 +110,33 @@ static void RecordEventsPromiseOutcome(EvalContext *ctx, const Promise *pp, Prom EvalContextLogPromiseIterationOutcome(ctx, pp, result); } +/* Returns the list of bundle references of the (expanded) 'then' attribute, + * which is always a list after expansion, see ExpandBundleListReference(). + * Logs an error and returns NULL if there is no 'then' attribute or it is + * empty. */ +static const Rlist *GetThenBundles(const Promise *pp) +{ + assert(pp != NULL); + + const Constraint *then_constraint = PromiseGetConstraint(pp, "then"); + if (then_constraint == NULL) + { + Log(LOG_LEVEL_ERR, "Reactor events promise '%s' does not specify a 'then' bundle", pp->promiser); + return NULL; + } + + assert(then_constraint->rval.type == RVAL_TYPE_LIST); + const Rlist *bundles = RvalRlistValue(then_constraint->rval); + if (bundles == NULL) + { + Log(LOG_LEVEL_ERR, "Reactor events promise '%s' has an empty list of 'then' bundles", pp->promiser); + } + return bundles; +} + // Temporary limitations: // - 'when' body: only a single constraint, 'file_deleted', is supported (no OR-ing of constraints yet) -// - 'then': only a single bundle is supported, not a list of bundles yet -// TODO: support multiple 'when' constraints and a list of 'then' bundles +// TODO: support multiple 'when' constraints static PromiseResult KeepEventsPromise(EvalContext *ctx, const Promise *pp) { assert(pp != NULL); @@ -127,16 +150,18 @@ static PromiseResult KeepEventsPromise(EvalContext *ctx, const Promise *pp) return PROMISE_RESULT_FAIL; } - const Constraint *then_constraint = PromiseGetConstraint(pp, "then"); - if (then_constraint == NULL) + const Rlist *then_bundles = GetThenBundles(pp); + if (then_bundles == NULL) { - Log(LOG_LEVEL_ERR, "Reactor events promise '%s' does not specify a 'then' bundle, ignoring", pp->promiser); return PROMISE_RESULT_FAIL; } - if (ResolveThenBundle(ctx, pp, then_constraint->rval, NULL) == NULL) + for (const Rlist *rp = then_bundles; rp != NULL; rp = rp->next) { - return PROMISE_RESULT_FAIL; + if (ResolveThenBundle(ctx, pp, rp->val, NULL) == NULL) + { + return PROMISE_RESULT_FAIL; + } } WatcherOptions opt = { @@ -205,33 +230,19 @@ static void EvaluateReactorBundle(EvalContext *ctx, const Bundle *bp) EvalContextStackPopFrame(ctx); } -/* Runs the bundle referred to by the 'then' attribute of the (expanded) - * events promise. */ -static PromiseResult RunThenBundle(EvalContext *ctx, const Promise *pp) +/* Runs the bundle referred to by an entry of the 'then' attribute of the + * (expanded) events promise. */ +static PromiseResult RunThenBundle(EvalContext *ctx, const Promise *pp, Rval then_rval) { assert(pp != NULL); - const Constraint *then_constraint = PromiseGetConstraint(pp, "then"); - if (then_constraint == NULL) - { - Log(LOG_LEVEL_ERR, "Reactor events promise '%s' does not specify a 'then' bundle", pp->promiser); - return PROMISE_RESULT_FAIL; - } - const Rlist *args = NULL; - const Bundle *bp = ResolveThenBundle(ctx, pp, then_constraint->rval, &args); + const Bundle *bp = ResolveThenBundle(ctx, pp, then_rval, &args); if (bp == NULL) { return PROMISE_RESULT_FAIL; } - /* The promise lock cache makes each promise act at most once per - * EvalContext, and the function cache keeps the results of functions like - * execresult(), but cf-reactor keeps the same EvalContext across events. - * Clear them so every event gets a fresh run of the bundle. */ - EvalContextPromiseLockCacheClear(ctx); - EvalContextFunctionCacheClear(ctx); - BundleBanner(bp, args); EvalContextSetBundleArgs(ctx, args); EvalContextStackPushBundleFrame(ctx, bp, args, false, NULL); @@ -254,6 +265,33 @@ static PromiseResult RunThenBundle(EvalContext *ctx, const Promise *pp) return result; } +/* Runs the bundles referred to by the 'then' attribute of the (expanded) + * events promise in order, like a bundlesequence. */ +static PromiseResult RunThenBundles(EvalContext *ctx, const Promise *pp) +{ + assert(pp != NULL); + + const Rlist *then_bundles = GetThenBundles(pp); + if (then_bundles == NULL) + { + return PROMISE_RESULT_FAIL; + } + + /* The promise lock cache makes each promise act at most once per + * EvalContext, and the function cache keeps the results of functions like + * execresult(), but cf-reactor keeps the same EvalContext across events. + * Clear them so every event gets a fresh run of the bundles. */ + EvalContextPromiseLockCacheClear(ctx); + EvalContextFunctionCacheClear(ctx); + + PromiseResult result = PROMISE_RESULT_NOOP; + for (const Rlist *rp = then_bundles; rp != NULL; rp = rp->next) + { + result = PromiseResultUpdate(result, RunThenBundle(ctx, pp, rp->val)); + } + return result; +} + typedef struct { const char *promise_hash; @@ -276,7 +314,7 @@ static PromiseResult KeepEventsPromiseOnEvent(EvalContext *ctx, const Promise *p return PROMISE_RESULT_SKIPPED; } - return RunThenBundle(ctx, pp); + return RunThenBundles(ctx, pp); } void HandleReactorEvent(EvalContext *ctx, const Promise *pp, const char *promise_hash, WatcherCheckResult check) diff --git a/libpromises/cf3.defs.h b/libpromises/cf3.defs.h index 3c90859fe6a..b9e79365f61 100644 --- a/libpromises/cf3.defs.h +++ b/libpromises/cf3.defs.h @@ -385,6 +385,7 @@ typedef enum CF_DATA_TYPE_REAL_RANGE, CF_DATA_TYPE_COUNTER, CF_DATA_TYPE_CONTAINER, + CF_DATA_TYPE_BUNDLE_LIST, CF_DATA_TYPE_NONE } DataType; diff --git a/libpromises/cf3parse_logic.h b/libpromises/cf3parse_logic.h index e3658711388..550d5e4cf44 100644 --- a/libpromises/cf3parse_logic.h +++ b/libpromises/cf3parse_logic.h @@ -466,7 +466,8 @@ static SyntaxTypeMatch CheckConstraint( /* For bodies and bundles definitions can be elsewhere, so they are checked in PolicyCheckRunnable(). */ if (bs[l].dtype != CF_DATA_TYPE_BODY - && bs[l].dtype != CF_DATA_TYPE_BUNDLE) + && bs[l].dtype != CF_DATA_TYPE_BUNDLE + && bs[l].dtype != CF_DATA_TYPE_BUNDLE_LIST) { return CheckConstraintTypeMatch( lval, diff --git a/libpromises/conversion.c b/libpromises/conversion.c index be4d9e52bd3..e95069bc33d 100644 --- a/libpromises/conversion.c +++ b/libpromises/conversion.c @@ -285,6 +285,7 @@ static const char *const datatype_strings[] = [CF_DATA_TYPE_REAL_RANGE] = "rrange", [CF_DATA_TYPE_COUNTER] = "counter", [CF_DATA_TYPE_CONTAINER] = "data", + [CF_DATA_TYPE_BUNDLE_LIST] = "blist", [CF_DATA_TYPE_NONE] = "none" }; diff --git a/libpromises/expand.c b/libpromises/expand.c index 27e2ca36701..dff8639f8f6 100644 --- a/libpromises/expand.c +++ b/libpromises/expand.c @@ -514,6 +514,39 @@ Rval ExpandBundleReference(EvalContext *ctx, return RvalNew(NULL, RVAL_TYPE_NOPROMISEE); } +/** + * Expand a bundle reference or a list of bundle references into a list of + * bundle references, like in a bundlesequence. Naked list variables (@(list)) + * are flattened into the result. Unlike EvaluateFinalRval(), function call + * entries are bundle calls and are never evaluated as functions. + */ +Rval ExpandBundleListReference(EvalContext *ctx, + const char *ns, const char *scope, + Rval rval) +{ + // Allocates new memory for the copy + switch (rval.type) + { + case RVAL_TYPE_SCALAR: + case RVAL_TYPE_FNCALL: + { + const Rlist list = { .val = rval, .next = NULL }; + return (Rval) { ExpandList(ctx, ns, scope, &list, true), RVAL_TYPE_LIST }; + } + + case RVAL_TYPE_LIST: + return (Rval) { ExpandList(ctx, ns, scope, RvalRlistValue(rval), true), + RVAL_TYPE_LIST }; + + case RVAL_TYPE_CONTAINER: + case RVAL_TYPE_NOPROMISEE: + return RvalNew(NULL, RVAL_TYPE_NOPROMISEE); + } + + assert(false); + return RvalNew(NULL, RVAL_TYPE_NOPROMISEE); +} + /** * Expand a #string into Buffer #out, returning the pointer to the string * itself, inside the Buffer #out. If #out is NULL then the buffer will be diff --git a/libpromises/expand.h b/libpromises/expand.h index 6a35c26edeb..0b6de6e6518 100644 --- a/libpromises/expand.h +++ b/libpromises/expand.h @@ -42,6 +42,7 @@ bool IsExpandable(const char *str); char *ExpandScalar(const EvalContext *ctx, const char *ns, const char *scope, const char *string, Buffer *out); Rval ExpandBundleReference(EvalContext *ctx, const char *ns, const char *scope, Rval rval); +Rval ExpandBundleListReference(EvalContext *ctx, const char *ns, const char *scope, Rval rval); Rval ExpandPrivateRval(const EvalContext *ctx, const char *ns, const char *scope, const void *rval_item, RvalType rval_type); Rlist *ExpandList(const EvalContext *ctx, const char *ns, const char *scope, const Rlist *list, int expandnaked); Rval EvaluateFinalRval(EvalContext *ctx, const Policy *policy, const char *ns, const char *scope, Rval rval, bool forcelist, const Promise *pp); diff --git a/libpromises/mod_reactor.c b/libpromises/mod_reactor.c index d07b0bd46cd..113a3e9a1a8 100644 --- a/libpromises/mod_reactor.c +++ b/libpromises/mod_reactor.c @@ -41,7 +41,7 @@ static const BodySyntax when_body = BodySyntaxNew("when", when_constraints, NULL static const ConstraintSyntax CF_EVENT_BODIES[] = { ConstraintSyntaxNewBody("when", &when_body, "Event to react to", SYNTAX_STATUS_NORMAL), - ConstraintSyntaxNewBundle("then", "Bundle to run on event", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewBundleList("then", "Bundle or list of bundles to run in order on event", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewNull() }; diff --git a/libpromises/policy.c b/libpromises/policy.c index 0d72d75c3d1..3daae23cf09 100644 --- a/libpromises/policy.c +++ b/libpromises/policy.c @@ -579,6 +579,7 @@ static bool RvalTypeCheckDataType(RvalType rval_type, DataType expected_datatype case CF_DATA_TYPE_OPTION_LIST: case CF_DATA_TYPE_REAL_LIST: case CF_DATA_TYPE_STRING_LIST: + case CF_DATA_TYPE_BUNDLE_LIST: return (rval_type == RVAL_TYPE_SCALAR) || (rval_type == RVAL_TYPE_LIST); case CF_DATA_TYPE_CONTAINER: @@ -2730,6 +2731,42 @@ static bool ValidateCustomPromise(const Promise *pp, Seq *errors) return valid; } +static bool BundleCallCheckArity(const Promise *pp, const Constraint *cp, Rval rval, Seq *errors) +{ + if (rval.type != RVAL_TYPE_FNCALL) + { + return true; + } + + // HACK: exploiting the fact that class-references and call-references are similar + FnCall *call = RvalFnCallValue(rval); + ClassRef ref = ClassRefParse(call->name); + if (!ClassRefIsQualified(ref)) + { + ClassRefQualify(&ref, PromiseGetNamespace(pp)); + } + + const Bundle *callee = PolicyGetBundle(PolicyFromPromise(pp), ref.ns, "agent", ref.name); + if (!callee) + { + callee = PolicyGetBundle(PolicyFromPromise(pp), ref.ns, "common", ref.name); + } + + ClassRefDestroy(ref); + + if (callee != NULL) + { + if (RlistLen(call->args) != RlistLen(callee->args)) + { + SeqAppend(errors, PolicyErrorNew(POLICY_ELEMENT_TYPE_CONSTRAINT, cp, + POLICY_ERROR_BUNDLE_CALL_ARITY, + call->name, RlistLen(callee->args), RlistLen(call->args))); + return false; + } + } + return true; +} + bool PromiseCheckBundleCallArity(const Promise *pp, const char *lval, Seq *errors) { assert(pp != NULL); @@ -2744,35 +2781,17 @@ bool PromiseCheckBundleCallArity(const Promise *pp, const char *lval, Seq *error // ensure: if call and callee are resolved, then they have matching arity if (StringEqual(cp->lval, lval)) { - if (cp->rval.type == RVAL_TYPE_FNCALL) + if (cp->rval.type == RVAL_TYPE_LIST) { - // HACK: exploiting the fact that class-references and call-references are similar - FnCall *call = RvalFnCallValue(cp->rval); - ClassRef ref = ClassRefParse(call->name); - if (!ClassRefIsQualified(ref)) + for (const Rlist *rp = RvalRlistValue(cp->rval); rp != NULL; rp = rp->next) { - ClassRefQualify(&ref, PromiseGetNamespace(pp)); - } - - const Bundle *callee = PolicyGetBundle(PolicyFromPromise(pp), ref.ns, "agent", ref.name); - if (!callee) - { - callee = PolicyGetBundle(PolicyFromPromise(pp), ref.ns, "common", ref.name); - } - - ClassRefDestroy(ref); - - if (callee) - { - if (RlistLen(call->args) != RlistLen(callee->args)) - { - SeqAppend(errors, PolicyErrorNew(POLICY_ELEMENT_TYPE_CONSTRAINT, cp, - POLICY_ERROR_BUNDLE_CALL_ARITY, - call->name, RlistLen(callee->args), RlistLen(call->args))); - success = false; - } + success &= BundleCallCheckArity(pp, cp, rp->val, errors); } } + else + { + success &= BundleCallCheckArity(pp, cp, cp->rval, errors); + } } } return success; diff --git a/libpromises/promises.c b/libpromises/promises.c index ec8b3d903c8..fb92c1923b9 100644 --- a/libpromises/promises.c +++ b/libpromises/promises.c @@ -561,6 +561,7 @@ static void AddDefaultBodiesToPromise(EvalContext *ctx, Promise *promise, const static bool EvaluateConstraintIteration(EvalContext *ctx, const Constraint *cp, Rval *rval_out) { + assert(cp != NULL); assert(cp->type == POLICY_ELEMENT_TYPE_PROMISE); const Promise *pp = cp->parent.promise; @@ -569,10 +570,15 @@ static bool EvaluateConstraintIteration(EvalContext *ctx, const Constraint *cp, return false; } - if (ExpectedDataType(cp->lval) == CF_DATA_TYPE_BUNDLE) + const DataType expected_type = ExpectedDataType(cp->lval); + if (expected_type == CF_DATA_TYPE_BUNDLE) { *rval_out = ExpandBundleReference(ctx, NULL, "this", cp->rval); } + else if (expected_type == CF_DATA_TYPE_BUNDLE_LIST) + { + *rval_out = ExpandBundleListReference(ctx, NULL, "this", cp->rval); + } else { *rval_out = EvaluateFinalRval(ctx, PromiseGetPolicy(pp), NULL, diff --git a/libpromises/rlist.c b/libpromises/rlist.c index 80fce58b266..3b997cfa1cc 100644 --- a/libpromises/rlist.c +++ b/libpromises/rlist.c @@ -64,6 +64,7 @@ RvalType DataTypeToRvalType(DataType datatype) case CF_DATA_TYPE_OPTION_LIST: case CF_DATA_TYPE_REAL_LIST: case CF_DATA_TYPE_STRING_LIST: + case CF_DATA_TYPE_BUNDLE_LIST: return RVAL_TYPE_LIST; case CF_DATA_TYPE_CONTAINER: diff --git a/libpromises/syntax.c b/libpromises/syntax.c index 7bbc28a1b30..0d15ff7e819 100644 --- a/libpromises/syntax.c +++ b/libpromises/syntax.c @@ -372,6 +372,7 @@ SyntaxTypeMatch CheckConstraintTypeMatch(const char *lval, Rval rval, DataType d case CF_DATA_TYPE_REAL_LIST: case CF_DATA_TYPE_CONTEXT_LIST: case CF_DATA_TYPE_OPTION_LIST: + case CF_DATA_TYPE_BUNDLE_LIST: break; default: return SYNTAX_TYPE_MATCH_ERROR_GOT_LIST; @@ -395,9 +396,15 @@ SyntaxTypeMatch CheckConstraintTypeMatch(const char *lval, Rval rval, DataType d case RVAL_TYPE_FNCALL: - /* Fn-like objects are assumed to be parameterized bundles in these... */ + /* Fn-like objects are parameterized bundles in bundle references... */ + if (dt == CF_DATA_TYPE_BUNDLE || dt == CF_DATA_TYPE_BUNDLE_LIST) + { + return SYNTAX_TYPE_MATCH_OK; + } + + /* ...and are assumed to be in these */ - checklist = SplitString("bundlesequence,edit_line,edit_xml,usebundle,service_bundle,home_bundle,then", ','); + checklist = SplitString("bundlesequence,edit_line,edit_xml,usebundle,service_bundle,home_bundle", ','); if (!IsItemIn(checklist, lval)) { @@ -434,6 +441,7 @@ SyntaxTypeMatch CheckConstraintTypeMatch(const char *lval, Rval rval, DataType d case CF_DATA_TYPE_BODY: case CF_DATA_TYPE_BUNDLE: + case CF_DATA_TYPE_BUNDLE_LIST: case CF_DATA_TYPE_CONTAINER: break; @@ -1005,13 +1013,16 @@ static SyntaxTypeMatch CheckFnCallType(const char *s, DataType desired_type) static JsonElement *ConstraintSyntaxToJson(const ConstraintSyntax *constraint_syntax) { + assert(constraint_syntax != NULL); JsonElement *json_constraint = JsonObjectCreate(5); JsonObjectAppendString(json_constraint, "attribute", constraint_syntax->lval); JsonObjectAppendString(json_constraint, "status", SyntaxStatusToString(constraint_syntax->status)); JsonObjectAppendString(json_constraint, "type", DataTypeToString(constraint_syntax->dtype)); - if (constraint_syntax->dtype != CF_DATA_TYPE_BODY && constraint_syntax->dtype != CF_DATA_TYPE_BUNDLE) + if (constraint_syntax->dtype != CF_DATA_TYPE_BODY + && constraint_syntax->dtype != CF_DATA_TYPE_BUNDLE + && constraint_syntax->dtype != CF_DATA_TYPE_BUNDLE_LIST) { JsonObjectAppendString(json_constraint, "range", constraint_syntax->range.validation_string); } diff --git a/libpromises/syntax.h b/libpromises/syntax.h index 95dc474e043..21413f702d9 100644 --- a/libpromises/syntax.h +++ b/libpromises/syntax.h @@ -114,6 +114,7 @@ JsonElement *SyntaxToJson(void); #define ConstraintSyntaxNewBody(lval, body_syntax, description, status) { lval, CF_DATA_TYPE_BODY, .range.body_type_syntax = body_syntax, description, status } #define ConstraintSyntaxNewBundle(lval, description, status) { lval, CF_DATA_TYPE_BUNDLE, .range.validation_string = CF_BUNDLE, description, status } +#define ConstraintSyntaxNewBundleList(lval, description, status) { lval, CF_DATA_TYPE_BUNDLE_LIST, .range.validation_string = CF_BUNDLE, description, status } #define BodySyntaxNew(body_type, constraints, check_fn, status) { body_type, constraints, check_fn, status } #define BodySyntaxNewNull() { NULL, NULL, NULL, SYNTAX_STATUS_NORMAL }