77
88import org .apache .logging .log4j .message .ParameterizedMessage ;
99import org .apache .logging .log4j .util .Supplier ;
10+ import org .elasticsearch .ElasticsearchException ;
1011import org .elasticsearch .ElasticsearchParseException ;
1112import org .elasticsearch .common .Nullable ;
13+ import org .elasticsearch .common .Strings ;
1214import org .elasticsearch .common .unit .TimeValue ;
15+ import org .elasticsearch .common .xcontent .ObjectPath ;
1316import org .elasticsearch .common .xcontent .ToXContentObject ;
1417import org .elasticsearch .common .xcontent .XContentBuilder ;
1518import org .elasticsearch .common .xcontent .XContentParser ;
1619import org .elasticsearch .license .XPackLicenseState ;
20+ import org .elasticsearch .script .JodaCompatibleZonedDateTime ;
1721import org .elasticsearch .xpack .core .watcher .actions .throttler .ActionThrottler ;
1822import org .elasticsearch .xpack .core .watcher .actions .throttler .Throttler ;
1923import org .elasticsearch .xpack .core .watcher .actions .throttler .ThrottlerField ;
3034import java .time .Clock ;
3135import java .time .ZoneOffset ;
3236import java .time .ZonedDateTime ;
37+ import java .util .ArrayList ;
38+ import java .util .Collection ;
39+ import java .util .Collections ;
40+ import java .util .HashMap ;
41+ import java .util .List ;
42+ import java .util .Map ;
3343import java .util .Objects ;
44+ import java .util .Set ;
45+ import java .util .stream .Collectors ;
3446
3547import static org .elasticsearch .common .unit .TimeValue .timeValueMillis ;
3648
3749public class ActionWrapper implements ToXContentObject {
3850
51+ private final int MAXIMUM_FOREACH_RUNS = 100 ;
52+
3953 private String id ;
4054 @ Nullable
4155 private final ExecutableCondition condition ;
4256 @ Nullable
4357 private final ExecutableTransform <Transform , Transform .Result > transform ;
4458 private final ActionThrottler throttler ;
4559 private final ExecutableAction <? extends Action > action ;
60+ @ Nullable
61+ private String path ;
4662
4763 public ActionWrapper (String id , ActionThrottler throttler ,
4864 @ Nullable ExecutableCondition condition ,
4965 @ Nullable ExecutableTransform <Transform , Transform .Result > transform ,
50- ExecutableAction <? extends Action > action ) {
66+ ExecutableAction <? extends Action > action ,
67+ @ Nullable String path ) {
5168 this .id = id ;
5269 this .condition = condition ;
5370 this .throttler = throttler ;
5471 this .transform = transform ;
5572 this .action = action ;
73+ this .path = path ;
5674 }
5775
5876 public String id () {
@@ -140,16 +158,90 @@ public ActionWrapperResult execute(WatchExecutionContext ctx) {
140158 return new ActionWrapperResult (id , conditionResult , null , new Action .Result .FailureWithException (action .type (), e ));
141159 }
142160 }
143- try {
144- Action .Result actionResult = action .execute (id , ctx , payload );
145- return new ActionWrapperResult (id , conditionResult , transformResult , actionResult );
146- } catch (Exception e ) {
147- action .logger ().error (
161+ if (Strings .isEmpty (path )) {
162+ try {
163+ Action .Result actionResult = action .execute (id , ctx , payload );
164+ return new ActionWrapperResult (id , conditionResult , transformResult , actionResult );
165+ } catch (Exception e ) {
166+ action .logger ().error (
167+ (Supplier <?>) () -> new ParameterizedMessage ("failed to execute action [{}/{}]" , ctx .watch ().id (), id ), e );
168+ return new ActionWrapperResult (id , new Action .Result .FailureWithException (action .type (), e ));
169+ }
170+ } else {
171+ try {
172+ List <Action .Result > results = new ArrayList <>();
173+ Object object = ObjectPath .eval (path , toMap (ctx ));
174+ int runs = 0 ;
175+ if (object instanceof Collection ) {
176+ Collection collection = Collection .class .cast (object );
177+ if (collection .isEmpty ()) {
178+ throw new ElasticsearchException ("foreach object [{}] was an empty list, could not run any action" , path );
179+ } else {
180+ for (Object o : collection ) {
181+ if (runs >= MAXIMUM_FOREACH_RUNS ) {
182+ break ;
183+ }
184+ if (o instanceof Map ) {
185+ results .add (action .execute (id , ctx , new Payload .Simple ((Map <String , Object >) o )));
186+ } else {
187+ results .add (action .execute (id , ctx , new Payload .Simple ("_value" , o )));
188+ }
189+ runs ++;
190+ }
191+ }
192+ } else if (object == null ) {
193+ throw new ElasticsearchException ("specified foreach object was null: [{}]" , path );
194+ } else {
195+ throw new ElasticsearchException ("specified foreach object was not a an array/collection: [{}]" , path );
196+ }
197+
198+ // check if we have mixed results, then set to partial failure
199+ final Set <Action .Result .Status > statuses = results .stream ().map (Action .Result ::status ).collect (Collectors .toSet ());
200+ Action .Result .Status status ;
201+ if (statuses .size () == 1 ) {
202+ status = statuses .iterator ().next ();
203+ } else {
204+ status = Action .Result .Status .PARTIAL_FAILURE ;
205+ }
206+
207+ final int numberOfActionsExecuted = runs ;
208+ return new ActionWrapperResult (id , conditionResult , transformResult ,
209+ new Action .Result (action .type (), status ) {
210+ @ Override
211+ public XContentBuilder toXContent (XContentBuilder builder , Params params ) throws IOException {
212+ builder .field ("number_of_actions_executed" , numberOfActionsExecuted );
213+ builder .startArray (WatchField .FOREACH .getPreferredName ());
214+ for (Action .Result result : results ) {
215+ builder .startObject ();
216+ result .toXContent (builder , params );
217+ builder .endObject ();
218+ }
219+ builder .endArray ();
220+ return builder ;
221+ }
222+ });
223+ } catch (Exception e ) {
224+ action .logger ().error (
148225 (Supplier <?>) () -> new ParameterizedMessage ("failed to execute action [{}/{}]" , ctx .watch ().id (), id ), e );
149- return new ActionWrapperResult (id , new Action .Result .FailureWithException (action .type (), e ));
226+ return new ActionWrapperResult (id , new Action .Result .FailureWithException (action .type (), e ));
227+ }
150228 }
151229 }
152230
231+ private Map <String , Object > toMap (WatchExecutionContext ctx ) {
232+ Map <String , Object > model = new HashMap <>();
233+ model .put ("id" , ctx .id ().value ());
234+ model .put ("watch_id" , ctx .id ().watchId ());
235+ model .put ("execution_time" , new JodaCompatibleZonedDateTime (ctx .executionTime ().toInstant (), ZoneOffset .UTC ));
236+ model .put ("trigger" , ctx .triggerEvent ().data ());
237+ model .put ("metadata" , ctx .watch ().metadata ());
238+ model .put ("vars" , ctx .vars ());
239+ if (ctx .payload ().data () != null ) {
240+ model .put ("payload" , ctx .payload ().data ());
241+ }
242+ return Collections .singletonMap ("ctx" , model );
243+ }
244+
153245 @ Override
154246 public boolean equals (Object o ) {
155247 if (this == o ) return true ;
@@ -186,6 +278,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
186278 .field (transform .type (), transform , params )
187279 .endObject ();
188280 }
281+ if (Strings .isEmpty (path ) == false ) {
282+ builder .field (WatchField .FOREACH .getPreferredName (), path );
283+ }
189284 builder .field (action .type (), action , params );
190285 return builder .endObject ();
191286 }
@@ -198,6 +293,7 @@ static ActionWrapper parse(String watchId, String actionId, XContentParser parse
198293 ExecutableCondition condition = null ;
199294 ExecutableTransform <Transform , Transform .Result > transform = null ;
200295 TimeValue throttlePeriod = null ;
296+ String path = null ;
201297 ExecutableAction <? extends Action > action = null ;
202298
203299 String currentFieldName = null ;
@@ -208,6 +304,8 @@ static ActionWrapper parse(String watchId, String actionId, XContentParser parse
208304 } else {
209305 if (WatchField .CONDITION .match (currentFieldName , parser .getDeprecationHandler ())) {
210306 condition = actionRegistry .getConditionRegistry ().parseExecutable (watchId , parser );
307+ } else if (WatchField .FOREACH .match (currentFieldName , parser .getDeprecationHandler ())) {
308+ path = parser .text ();
211309 } else if (Transform .TRANSFORM .match (currentFieldName , parser .getDeprecationHandler ())) {
212310 transform = actionRegistry .getTransformRegistry ().parse (watchId , parser );
213311 } else if (ThrottlerField .THROTTLE_PERIOD .match (currentFieldName , parser .getDeprecationHandler ())) {
@@ -235,7 +333,7 @@ static ActionWrapper parse(String watchId, String actionId, XContentParser parse
235333 }
236334
237335 ActionThrottler throttler = new ActionThrottler (clock , throttlePeriod , licenseState );
238- return new ActionWrapper (actionId , throttler , condition , transform , action );
336+ return new ActionWrapper (actionId , throttler , condition , transform , action , path );
239337 }
240338
241339}
0 commit comments