From 446280ab6eb743054364ac27456899251f1bdc8d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Emmanuel=20Beno=C3=AEt?= <tseeker@nocternity.net>
Date: Sat, 17 Sep 2022 12:22:40 +0200
Subject: [PATCH] run_once clause

When the run_once clause is present and set to a truthy value, the
instruction it is attached to will only be executed the first time it is
encountered.
---
 README.md                          |  6 +++---
 example/01-test-reconstructed.yml  | 15 +++++++++------
 inventory_plugins/reconstructed.py | 16 +++++++++++++++-
 3 files changed, 27 insertions(+), 10 deletions(-)

diff --git a/README.md b/README.md
index ca1b399..d96800d 100644
--- a/README.md
+++ b/README.md
@@ -8,9 +8,9 @@ I just don't forget about this whole thing.
 
 A `reconstructed` inventory executes a list of instructions that is read
 from the `instructions` YAML field. Each instruction is a table with some
-minimal control flow (`when` and `loop` keywords that work mostly like their
-playbook cousins), an `action` field that contains the name of the instruction
-to execute, and whatever fields are needed for the instruction.
+minimal control flow (`when`, `loop` and `run_once` keywords that work mostly
+like their playbook cousins), an `action` field that contains the name of the
+instruction to execute, and whatever fields are needed for the instruction.
 
 The following actions are supported:
 
diff --git a/example/01-test-reconstructed.yml b/example/01-test-reconstructed.yml
index f79185e..077d24f 100644
--- a/example/01-test-reconstructed.yml
+++ b/example/01-test-reconstructed.yml
@@ -30,12 +30,15 @@ instructions:
       - action: stop
 
   # Only create the managed groups if we *have* managed hosts
-  - action: create_group
-    group: managed
-  - loop: [by_environment, by_network, by_failover_stack, by_service]
-    action: create_group
-    group: "{{ item }}"
-    parent: managed
+  - action: block
+    run_once: true
+    block:
+      - action: create_group
+        group: managed
+      - loop: [by_environment, by_network, by_failover_stack, by_service]
+        action: create_group
+        group: "{{ item }}"
+        parent: managed
 
   # Copy inv__data fields to separate inv__ variables
   - loop:
diff --git a/inventory_plugins/reconstructed.py b/inventory_plugins/reconstructed.py
index 5cd2656..c3592b8 100644
--- a/inventory_plugins/reconstructed.py
+++ b/inventory_plugins/reconstructed.py
@@ -40,6 +40,8 @@ DOCUMENTATION = """
         - The C(when) field, if present, must contain a Jinja expression
           representing a condition which will be checked before the instruction
           is executed.
+        - The C(run_once) field will ensure that the instuction it is attached
+          to will only run one time at most.
         - The C(action) field must be set to one of the following values.
         - The C(block) action is another form of flow control, which can be
           used to repeat multiple instructions or make them obey a single
@@ -91,7 +93,7 @@ DOCUMENTATION = """
         default: host
 """
 
-INSTR_COMMON_FIELDS = ("when", "loop", "loop_var", "action")
+INSTR_COMMON_FIELDS = ("when", "loop", "loop_var", "action", "run_once")
 """Fields that may be present on all instructions."""
 
 INSTR_OWN_FIELDS = {
@@ -231,6 +233,7 @@ class RcInstruction(abc.ABC):
         self._loop = None
         self._loop_var = None
         self._action = action
+        self._executed_once = None
 
     def __repr__(self):
         """Builds a compact debugging representation of the instruction, \
@@ -242,6 +245,8 @@ class RcInstruction(abc.ABC):
             flow.append(
                 "loop=%s, loop_var=%s" % (repr(self._loop), repr(self._loop_var))
             )
+        if self._executed_once is not None:
+            flow.append("run_once")
         if flow:
             output = "{%s}" % (", ".join(flow),)
         else:
@@ -269,6 +274,8 @@ class RcInstruction(abc.ABC):
             output.append("{when: %s}" % (repr(self._condition),))
         if self._loop is not None:
             output.append("{loop[%s]: %s}" % (self._loop_var, repr(self._loop)))
+        if self._executed_once is not None:
+            output.append("{run_once}")
         output.extend(self.dump_instruction())
         return output
 
@@ -331,6 +338,9 @@ class RcInstruction(abc.ABC):
             raise AnsibleParserError(
                 "%s: 'loop_var' clause found without 'loop'" % (self._action,)
             )
+        # Handle instructions that may only be executed once
+        if record.get("run_once", False):
+            self._executed_once = False
         # Process action-specific fields
         self.parse_action(record)
 
@@ -395,6 +405,10 @@ class RcInstruction(abc.ABC):
             ``True`` if execution must continue, ``False`` if it must be
             interrupted
         """
+        if self._executed_once is True:
+            return True
+        if self._executed_once is False:
+            self._executed_once = True
         if self._loop is None:
             self._display.vvvv("%s : running action %s" % (host_name, self._action))
             return self.run_iteration(host_name, variables)