Skip to content

Commit f43775a

Browse files
committed
Simplify hash handling, consolidate on single currentHash
Signed-off-by: Yury Tsarev <yury@upbound.io>
1 parent fd92aa6 commit f43775a

File tree

9 files changed

+91
-203
lines changed

9 files changed

+91
-203
lines changed

README.md

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ A Crossplane Composition Function for implementing manual approval workflows.
77
The `function-approve` provides a serverless approval mechanism at the Crossplane level that:
88

99
1. Tracks changes to a specified field by computing a hash
10-
2. When changes need approval, overwrites Desired state with Observed state to prevent changes
10+
2. When changes need approval, uses fatal results to halt pipeline execution
1111
3. Requires explicit approval before allowing changes to proceed
1212
4. Prevents drift by storing previously approved state
1313

14-
This function implements the approval workflow using entirely Crossplane-native mechanisms without external dependencies, making it lightweight and reliable.
14+
This function implements the approval workflow using Crossplane's fatal result mechanism, ensuring that no changes are applied until explicitly approved.
1515

1616
## Usage
1717

@@ -29,10 +29,10 @@ spec:
2929
### How It Works
3030
3131
1. When a resource is created or updated, `function-approve` calculates a hash of the monitored field (e.g., `spec.resources`).
32-
2. The function stores this hash in `status.newHash` (or specified field).
33-
3. If there's a previous approved hash (`status.oldHash`) and it doesn't match the new hash, the function replaces Desired state with Observed state.
32+
2. The function compares this hash with the previously approved hash stored in `status.currentHash`.
33+
3. If the hashes don't match and approval is not granted, the function returns a fatal result to halt pipeline execution.
3434
4. An operator must approve the change by setting `status.approved = true`.
35-
5. After approval, the new hash is stored as the approved hash, the approval flag is reset, and changes are allowed to proceed.
35+
5. After approval, the new hash is stored as `currentHash`, the approval flag is reset, and changes are allowed to proceed.
3636
6. If a customer modifies an existing claim after approval, this will generate a new hash, requiring another approval.
3737

3838
## Example
@@ -55,9 +55,7 @@ spec:
5555
kind: Input
5656
dataField: "spec.resources" # Field to monitor for changes
5757
approvalField: "status.approved"
58-
newHashField: "status.newHash"
59-
oldHashField: "status.oldHash"
60-
pauseAnnotation: "crossplane.io/paused"
58+
currentHashField: "status.currentHash"
6159
detailedCondition: true
6260
```
6361

@@ -68,8 +66,7 @@ spec:
6866
| `dataField` | string | **Required**. Field to monitor for changes (e.g., `spec.resources`) |
6967
| `hashAlgorithm` | string | Algorithm to use for hash calculation. Supported values: `md5`, `sha256`, `sha512`. Default: `sha256` |
7068
| `approvalField` | string | Status field to check for approval. Default: `status.approved` |
71-
| `oldHashField` | string | Status field to store previously approved hash. Default: `status.oldHash` |
72-
| `newHashField` | string | Status field to store current hash. Default: `status.newHash` |
69+
| `currentHashField` | string | Status field to store the approved hash. Default: `status.currentHash` |
7370
| `detailedCondition` | bool | Whether to add detailed information to conditions. Default: `true` |
7471
| `approvalMessage` | string | Message to display when approval is required. Default: `Changes detected. Approval required.` |
7572

@@ -107,32 +104,30 @@ spec:
107104
approved:
108105
type: boolean
109106
description: "Whether the current changes are approved"
110-
oldHash:
107+
currentHash:
111108
type: string
112-
description: "Hash of previously approved resource state"
113-
newHash:
114-
type: string
115-
description: "Hash of current resource state"
109+
description: "Hash of the currently approved resource state"
116110
```
117111

118112
## Approving Changes
119113

120-
When changes are detected, the Desired state is replaced with Observed state (preventing any changes from being applied), and the resource will show an `ApprovalRequired` condition. To approve the changes, patch the resource's status:
114+
When changes are detected, the function returns a fatal result (halting pipeline execution) and the resource will show an `ApprovalRequired` condition. To approve the changes, patch the resource's status:
121115

122116
```yaml
123117
kubectl patch xapproval example --type=merge --subresource=status -p '{"status":{"approved":true}}'
124118
```
125119

126120
After approval, the function will:
127-
1. Record the new state as the approved state
128-
2. Allow the changes to proceed normally without overwriting the Desired state
121+
1. Update `currentHash` to the new approved hash
122+
2. Reset the approval flag to `false`
123+
3. Allow the pipeline to continue normally
129124

130125
## Resetting Approval State
131126

132-
If you need to reset the approval state, you can clear the `oldHash` field:
127+
If you need to reset the approval state, you can clear the `currentHash` field:
133128

134129
```yaml
135-
kubectl patch xapproval example --type=merge --subresource=status -p '{"status":{"oldHash":""}}'
130+
kubectl patch xapproval example --type=merge --subresource=status -p '{"status":{"currentHash":""}}'
136131
```
137132

138133
## Security Considerations
@@ -142,19 +137,18 @@ kubectl patch xapproval example --type=merge --subresource=status -p '{"status":
142137

143138
## How Changes Are Prevented
144139

145-
The function uses a direct approach to prevent changes when approval is required:
140+
The function uses fatal results to prevent changes when approval is required:
146141

147142
1. When changes are detected but not yet approved, the function:
148-
- Replaces the Desired composite resource with the Observed resource
149-
- Replaces any Desired composed resources with the Observed resources
143+
- Returns a fatal result to halt pipeline execution
150144
- Sets an ApprovalRequired condition for visibility
145+
- Provides detailed information about the required approval
151146

152147
2. This approach has several benefits:
153-
- Deterministic behavior - changes are physically prevented
154-
- No reliance on pause annotations or condition interpretation by the reconciler
148+
- Deterministic behavior - pipeline execution is completely stopped
149+
- Clear error messaging to operators about required approvals
155150
- Works consistently across different Crossplane versions
156-
- Clear separation of approval status and reconciliation mechanics
157-
- Carefully preserves required metadata fields to avoid e2e testing issues
151+
- Clean separation between approval logic and resource state
158152

159153
## Complete Example
160154

@@ -179,8 +173,7 @@ spec:
179173
dataField: "spec.resources"
180174
approvalField: "status.approved"
181175
hashAlgorithm: "sha256"
182-
newHashField: "status.currentHash"
183-
oldHashField: "status.approvedHash"
176+
currentHashField: "status.currentHash"
184177
detailedCondition: true
185178
approvalMessage: "Cluster changes require admin approval"
186179
- step: create-resources

example/README.md

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,61 +21,48 @@ The function works by:
2121

2222
## Running the Examples
2323

24-
### Basic Example with Pause Annotation
24+
### Running the Example
2525

26-
The default behavior uses the `crossplane.io/paused` annotation to pause reconciliation:
26+
Render the example to see how the approval function works:
2727

2828
```shell
2929
crossplane render xr.yaml composition.yaml functions.yaml
3030
```
3131

32-
### Example with Synced=False Condition
33-
34-
This alternative approach uses the Synced=False condition instead of the pause annotation:
35-
36-
```shell
37-
crossplane render xr.yaml composition.yaml functions.yaml
38-
```
39-
40-
The `setSyncedFalse: true` option in the composition enables this behavior.
32+
The function will detect changes and require approval before allowing the pipeline to continue.
4133

4234
## Configuration Options
4335

4436
The function supports these configuration options:
4537

4638
- `dataField`: Specifies which field to monitor for changes (required)
4739
- `approvalField`: Status field to check for approval (default: "status.approved")
48-
- `oldHashField`: Where to store the approved hash (default: "status.oldHash")
49-
- `newHashField`: Where to store the current hash (default: "status.newHash")
50-
- `pauseAnnotation`: Annotation used to pause reconciliation (default: "crossplane.io/paused")
40+
- `currentHashField`: Where to store the approved hash (default: "status.currentHash")
5141
- `detailedCondition`: Whether to include hash details in conditions (default: true)
5242
- `approvalMessage`: Custom message for approval required condition
53-
- `setSyncedFalse`: Use Synced=False condition instead of pause annotation (default: false)
5443

5544
## Approval Workflow
5645

5746
1. Make changes to the resource's spec
58-
2. The function detects changes and pauses reconciliation
59-
3. Review the changes through the resource's conditions
47+
2. The function detects changes and halts the pipeline execution
48+
3. Review the changes through the resource's conditions and fatal results
6049
4. Set the approval field (default: `status.approved: true`)
61-
5. The function detects approval and resumes reconciliation
50+
5. The function detects approval and allows the pipeline to continue
51+
6. The `currentHash` is updated to reflect the newly approved state
6252

63-
## Example With setSyncedFalse
53+
## Example Configuration
6454

65-
In some environments, it may be preferable to use Synced=False condition instead of annotations.
66-
The `setSyncedFalse: true` option enables this alternative approach:
55+
Here's a typical function configuration:
6756

6857
```yaml
6958
apiVersion: approve.fn.crossplane.io/v1alpha1
7059
kind: Input
7160
dataField: "spec.resources"
7261
approvalField: "status.approved"
73-
newHashField: "status.newHash"
74-
oldHashField: "status.oldHash"
62+
currentHashField: "status.currentHash"
7563
detailedCondition: true
7664
approvalMessage: "Changes detected. Administrative approval required."
77-
setSyncedFalse: true
7865
```
7966
80-
When enabled, this sets the Synced condition to False instead of adding the pause annotation,
81-
providing the same pause functionality through a different mechanism.
67+
The function uses a fatal result approach to halt pipeline execution when approval is required,
68+
ensuring that no changes are applied until explicitly approved.

example/composition.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ spec:
3535
kind: Input
3636
dataField: "spec.resources"
3737
approvalField: "status.approved"
38-
newHashField: "status.newHash"
39-
oldHashField: "status.oldHash"
38+
currentHashField: "status.currentHash"
4039
detailedCondition: true
4140
approvalMessage: "Changes detected. Administrative approval required."

example/xr.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ spec:
1414
environment: development
1515
status:
1616
approved: true
17-
oldHash: 2ce2TEST269e18bfb1c
17+
currentHash: c48da99a2611a5c0ee7fcdf769a14307a2fbf4c6ec160f945c5837a0b5fc75bd

0 commit comments

Comments
 (0)