|
| 1 | +# 17. Agent Operations Framework |
| 2 | + |
| 3 | +Date: 2025-01-27 |
| 4 | + |
| 5 | +## Status |
| 6 | + |
| 7 | +Accepted |
| 8 | + |
| 9 | +## Context |
| 10 | + |
| 11 | +Following the new epics and product decisions, we need to build a framework capable of executing write operations on SAP machines. |
| 12 | +To achieve this, we have decided to use the agent, as it runs on the machines with root privileges, has all the necessary permissions to execute commands, and includes infrastructure to consume messages from other components of Trento. |
| 13 | + |
| 14 | +Unlike fact-gathering operations, we have chosen to delegate this new development to a dedicated library rather than embedding the code directly into the agent's codebase. This approach ensures better separation of concerns and allows the release of additional tools supporting operations without affecting the agent's codebase. |
| 15 | + |
| 16 | +The requirements for this new development are very specific: |
| 17 | + |
| 18 | +- Operations must be atomic and include rollback capabilities. |
| 19 | +- Operations must accept arguments to enable actions in different contexts without requiring dedicated code for each case. |
| 20 | +- Operations must be transactional, including distinct steps for prerequisites verification, commit, rollback, and validation of changes applied during the commit phase. |
| 21 | +- Operations must be versioned, with backward compatibility ensured for previous versions in the event of updates. |
| 22 | +- Operations must be idempotent. |
| 23 | + |
| 24 | +The agent will consume the library containing these operations and use it to fulfill operation requests from other components. |
| 25 | + |
| 26 | +## Decision |
| 27 | + |
| 28 | +The library handling operations is named [workbench](https://github.com/trento-project/workbench). |
| 29 | + |
| 30 | +## Glossary |
| 31 | + |
| 32 | +- **Operator**: A component of the workbench library responsible for performing operations on the machine. It consumes arguments and implements the callbacks required to ensure operations are transactional. |
| 33 | +- **Operation**: The specific task to be executed on the machine. |
| 34 | +- **Registry**: A component responsible for providing operators when requested by third parties consuming the library. It manages versioning. |
| 35 | +- **Executor**: A component that manages the transactional nature of operations. It wraps an operator and invokes its methods at the appropriate time, ensuring transactional execution. |
| 36 | + |
| 37 | +### Example: |
| 38 | + |
| 39 | +- **Operator**: `saptuneapplysolution` - Applies a SAP Tune solution using the solution name as an argument. |
| 40 | +- **Operation**: Apply a SAP Tune solution if it has not already been applied. If already applied, no action is taken. In case of an error, revert the solution specified as an argument. |
| 41 | + |
| 42 | +## Operator |
| 43 | + |
| 44 | +An operator is a unit of code capable of performing write operations on target machines. |
| 45 | +A write operation can either succeed or fail. Upon success, it generates a diff showing changes made during the commit phase. |
| 46 | + |
| 47 | +The operator accepts arguments in the form of a `map[string]any` to specify operation parameters. Each operator is responsible for extracting and validating these arguments. |
| 48 | + |
| 49 | +The operator follows a transactional workflow, which includes the following distinct phases: |
| 50 | + |
| 51 | +- **PLAN** |
| 52 | +- **COMMIT** |
| 53 | +- **VERIFY** |
| 54 | +- **ROLLBACK** |
| 55 | + |
| 56 | +### PLAN |
| 57 | + |
| 58 | +The PLAN phase collects information about the operation and verifies prerequisites. |
| 59 | +This phase also gathers information for generating diffs by collecting the "before" state of the system. |
| 60 | +Backups are created for any resources modified during the COMMIT phase, ensuring restoration is possible in case of rollback or manual recovery if rollback fails. |
| 61 | + |
| 62 | +If an error occurs during the PLAN phase, no rollback is required; the operation is simply aborted. |
| 63 | + |
| 64 | +### COMMIT |
| 65 | + |
| 66 | +The COMMIT phase performs the actual write operations using the data collected during the PLAN phase. |
| 67 | +If an error occurs, rollback is triggered. |
| 68 | + |
| 69 | +The COMMIT phase must be idempotent. If a requested change has already been applied, the commit operation is skipped without error. Idempotency must be implemented based on the specific operation's requirements. |
| 70 | + |
| 71 | +### VERIFY |
| 72 | + |
| 73 | +The VERIFY phase ensures the actions applied during the COMMIT phase produced the expected results. |
| 74 | +If an error occurs, rollback is initiated. |
| 75 | + |
| 76 | +The VERIFY phase also collects the "after" state to generate the diff showing changes applied during the commit. |
| 77 | + |
| 78 | +### ROLLBACK |
| 79 | + |
| 80 | +The ROLLBACK phase reverts changes made during the COMMIT phase. It uses data collected during the PLAN phase to restore the system to its previous state. |
| 81 | + |
| 82 | +Rollback implementations may vary based on the type of operation. Clear error messages and appropriate logs must be provided. |
| 83 | + |
| 84 | +If rollback fails, an error is returned without further action. |
| 85 | + |
| 86 | +Each operator implements these phases by satisfying the `phaser` interface: |
| 87 | + |
| 88 | +```go |
| 89 | +type phaser interface { |
| 90 | + plan(ctx context.Context) error |
| 91 | + commit(ctx context.Context) error |
| 92 | + rollback(ctx context.Context) error |
| 93 | + verify(ctx context.Context) error |
| 94 | + operationDiff(ctx context.Context) map[string]any |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | + |
| 99 | +These methods are invoked by the Executor, which wraps the operator. All operators are exposed through a constructor function, returning operators already wrapped in an Executor. |
| 100 | +Executor |
| 101 | +The Executor is a wrapper around an operator that manages operations transactionally. |
| 102 | +For library users, the Executor is transparent—operators are already wrapped within an Executor. |
| 103 | + |
| 104 | +Below is a flowchart illustrating the transactional flow: |
| 105 | + |
| 106 | + |
| 107 | + |
| 108 | +## Registry |
| 109 | + |
| 110 | +The Registry manages all available operators. |
| 111 | +Each operator has a version. By default, the latest version is fetched if no specific version is requested. |
| 112 | + |
| 113 | +The operator naming convention is: `<operatorname>@<version>` |
| 114 | + |
| 115 | +The Registry returns an Operator Builder: |
| 116 | + |
| 117 | +```golang |
| 118 | +type OperatorBuilder func(operationID string, arguments OperatorArguments) Operator |
| 119 | +``` |
| 120 | + |
| 121 | +- `operationID`: A unique identifier for the operation. |
| 122 | +- `arguments`: A `map[string]any` structure containing operation parameters. |
| 123 | + |
| 124 | +## Consequences |
| 125 | + |
| 126 | +This development will enable transactional write operations on target machines. |
| 127 | + |
| 128 | +Each operation is atomic. |
| 129 | +Coordination, ordering, and dependency management of multiple operations are not the agent's responsibility but are delegated to another component that orchestrates their execution. |
0 commit comments