-
Notifications
You must be signed in to change notification settings - Fork 229
Add comprehensive documentation for resource extension points #5004
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Copilot
wants to merge
11
commits into
main
Choose a base branch
from
copilot/add-docs-for-extension-points
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 6 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
27da1cb
Initial plan
Copilot eee496c
Add documentation for 5 extension points
Copilot 5a324d2
Add documentation for remaining extension points
Copilot 2e6ab43
Enhance code comments for extension point interfaces
Copilot 4a619aa
Add extension points to contributing index
Copilot c04a5c3
Add documentation for PreReconciliationOwnerChecker extension
Copilot df58f62
Address PR review feedback
Copilot 3583cf2
Fix links
theunrepentantgeek e06e9e9
Merge branch 'main' into copilot/add-docs-for-extension-points
theunrepentantgeek c8af1ad
Merge branch 'main' into copilot/add-docs-for-extension-points
theunrepentantgeek acb7acd
Remove broken link
theunrepentantgeek File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| --- | ||
| title: Extension Points | ||
| linktitle: Extension Points | ||
| weight: 100 | ||
| menu: | ||
| main: | ||
| parent: Contributing | ||
| description: "How to extend Azure Service Operator v2 resources with custom behavior" | ||
| --- | ||
|
|
||
| Azure Service Operator v2 provides several extension points that allow customization of resource behavior beyond what is generated from Azure OpenAPI specifications. These extension points enable contributors to add custom logic at various stages of the resource lifecycle. | ||
|
|
||
| ## Overview | ||
|
|
||
| Extension points are Go interfaces defined in `v2/pkg/genruntime/extensions/` that resources can implement to customize their behavior. When a resource implements an extension interface, the controller will invoke the custom logic at the appropriate time during reconciliation. | ||
|
|
||
| ### Extension Implementation Pattern | ||
|
|
||
| Extensions are typically implemented in resource-specific files under `v2/api/<service>/customizations/<resource>_extensions.go`. The general pattern is: | ||
|
|
||
| 1. Declare that your extension type implements the interface: | ||
| ```go | ||
| var _ extensions.ARMResourceModifier = &MyResourceExtension{} | ||
| ``` | ||
|
|
||
| 2. Implement the required method(s) of the interface | ||
|
|
||
| 3. The controller automatically detects and uses the extension through type assertion | ||
theunrepentantgeek marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ### Available Extension Points | ||
|
|
||
| The following extension points are available for customizing resource behavior: | ||
|
|
||
| | Extension Point | Purpose | When Invoked | | ||
| |-----------------|---------|--------------| | ||
| | [ARMResourceModifier]({{< relref "arm-resource-modifier" >}}) | Modify the ARM payload before sending to Azure | Just before PUT/PATCH to ARM | | ||
| | [Claimer]({{< relref "claimer" >}}) | Customize resource claiming logic | During resource ownership claim | | ||
| | [Deleter]({{< relref "deleter" >}}) | Customize resource deletion behavior | When resource is being deleted | | ||
| | [ErrorClassifier]({{< relref "error-classifier" >}}) | Classify ARM errors as retryable or fatal | When ARM returns an error | | ||
| | [Importer]({{< relref "importer" >}}) | Customize resource import behavior | During `asoctl import` operations | | ||
| | [KubernetesSecretExporter]({{< relref "kubernetes-secret-exporter" >}}) | Export secrets to Kubernetes | After successful reconciliation | | ||
| | [PostReconciliationChecker]({{< relref "post-reconciliation-checker" >}}) | Perform post-reconciliation validation | After ARM reconciliation succeeds | | ||
| | [PreReconciliationChecker]({{< relref "pre-reconciliation-checker" >}}) | Validate before reconciling | Before sending requests to ARM | | ||
| | [PreReconciliationOwnerChecker]({{< relref "pre-reconciliation-owner-checker" >}}) | Validate owner state before reconciling | Before any ARM operations (including GET) | | ||
| | [SuccessfulCreationHandler]({{< relref "successful-creation-handler" >}}) | Handle successful resource creation | After initial resource creation | | ||
|
|
||
| ## When to Use Extensions | ||
|
|
||
| Extensions should be used when: | ||
|
|
||
| - The generated code doesn't handle a specific Azure resource quirk | ||
| - Additional validation or logic is needed before/after ARM operations | ||
| - Custom error handling is required for specific scenarios | ||
| - Resources need special handling during creation, deletion, or import | ||
| - Secrets or configuration need custom export logic | ||
|
|
||
| Extensions should **not** be used for: | ||
|
|
||
| - Changes that could be made to the generator itself | ||
| - Logic that applies to all resources (consider modifying the controller instead) | ||
| - Working around bugs in the generator (fix the generator instead) | ||
|
|
||
| ## Development Guidelines | ||
|
|
||
| 1. **Keep extensions minimal**: Only add logic that cannot be handled by the generator | ||
| 2. **Document thoroughly**: Explain why the extension is needed | ||
| 3. **Type assert hub versions**: Include hub type assertions to catch breaking changes | ||
| 4. **Handle errors gracefully**: Return appropriate error types and messages | ||
| 5. **Test thoroughly**: Add unit tests for extension logic | ||
| 6. **Call next**: Most extensions use a chain pattern - remember to call the `next` function | ||
|
|
||
| ## Related Resources | ||
|
|
||
| - [Adding a new code-generated resource]({{< relref "../add-a-new-code-generated-resource" >}}) | ||
| - [Generator overview]({{< relref "../generator-overview" >}}) | ||
| - [Testing]({{< relref "../testing" >}}) | ||
243 changes: 243 additions & 0 deletions
243
docs/hugo/content/contributing/extension-points/arm-resource-modifier.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,243 @@ | ||
| --- | ||
| title: ARMResourceModifier | ||
| linktitle: ARMResourceModifier | ||
| weight: 10 | ||
| --- | ||
|
|
||
| ## Description | ||
|
|
||
| `ARMResourceModifier` allows resources to modify the payload that will be sent to Azure Resource Manager (ARM) immediately before it is transmitted. This extension point is invoked after the standard resource conversion and validation, but before the HTTP request is made to ARM. | ||
|
|
||
| The interface is invoked during PUT and PATCH operations to ARM, giving the resource an opportunity to make last-minute adjustments to the ARM payload based on runtime conditions, Azure state, or complex business logic that cannot be expressed in the generated code. | ||
|
|
||
| ## Interface Definition | ||
|
|
||
| ```go | ||
| type ARMResourceModifier interface { | ||
| ModifyARMResource( | ||
| ctx context.Context, | ||
| armClient *genericarmclient.GenericClient, | ||
| armObj genruntime.ARMResource, | ||
| obj genruntime.ARMMetaObject, | ||
| kubeClient kubeclient.Client, | ||
| resolver *resolver.Resolver, | ||
| log logr.Logger, | ||
| ) (genruntime.ARMResource, error) | ||
| } | ||
| ``` | ||
|
|
||
| **Parameters:** | ||
| - `ctx`: The current operation context | ||
| - `armClient`: Client for making additional ARM API calls if needed | ||
| - `armObj`: The ARM resource representation about to be sent to Azure | ||
| - `obj`: The Kubernetes resource being reconciled | ||
| - `kubeClient`: Client for accessing the Kubernetes cluster | ||
| - `resolver`: Helper for resolving resource references | ||
| - `log`: Logger for the current operation | ||
|
|
||
| **Returns:** | ||
| - Modified `genruntime.ARMResource` that will be sent to ARM | ||
| - Error if modification fails (will block the ARM request) | ||
|
|
||
| ## Motivation | ||
|
|
||
| The `ARMResourceModifier` extension exists to handle cases where: | ||
|
|
||
| 1. **Azure resource state affects the payload**: Some Azure resources require different payloads based on their current state in Azure (e.g., creation vs. update) | ||
|
|
||
| 2. **Complex conditional logic**: Business logic that depends on multiple factors and cannot be expressed declaratively in the resource schema | ||
|
|
||
| 3. **Azure-specific quirks**: Handling special cases or undocumented Azure behavior that varies by resource type | ||
|
|
||
| 4. **Dynamic payload construction**: Building parts of the payload at runtime based on information retrieved from Azure or Kubernetes | ||
|
|
||
| 5. **Soft-delete scenarios**: Resources with soft-delete capabilities (like Key Vault) may need special handling to recover or purge existing resources | ||
|
|
||
| ## When to Use | ||
|
|
||
| Implement `ARMResourceModifier` when: | ||
|
|
||
| - ✅ The resource has different creation modes or requires conditional field population | ||
| - ✅ You need to query Azure or Kubernetes state to determine the correct payload | ||
| - ✅ The resource has soft-delete and requires recovery or purge logic | ||
| - ✅ Child resources need to be included in parent payloads (e.g., VNET subnets) | ||
| - ✅ Field values must be computed at reconciliation time based on external state | ||
|
|
||
| Do **not** use `ARMResourceModifier` when: | ||
|
|
||
| - ❌ The logic could be handled by field defaults or validation | ||
| - ❌ The change should apply to all resources (modify the generator instead) | ||
| - ❌ Simple field transformations (use conversion functions) | ||
| - ❌ The issue is a generator bug (fix the generator) | ||
|
|
||
| ## Example: Key Vault CreateMode Handling | ||
|
|
||
| The Key Vault resource uses `ARMResourceModifier` to handle different creation modes based on whether a soft-deleted vault exists: | ||
|
|
||
| ```go | ||
| var _ extensions.ARMResourceModifier = &VaultExtension{} | ||
|
|
||
| func (ex *VaultExtension) ModifyARMResource( | ||
| ctx context.Context, | ||
| armClient *genericarmclient.GenericClient, | ||
| armObj genruntime.ARMResource, | ||
| obj genruntime.ARMMetaObject, | ||
| kubeClient kubeclient.Client, | ||
| resolver *resolver.Resolver, | ||
| log logr.Logger, | ||
| ) (genruntime.ARMResource, error) { | ||
| // Type assert to the specific resource type | ||
| kv, ok := obj.(*keyvault.Vault) | ||
| if !ok { | ||
| return nil, eris.Errorf( | ||
| "Cannot run VaultExtension.ModifyARMResource() with unexpected resource type %T", | ||
| obj) | ||
| } | ||
|
|
||
| // Type assert hub version to catch breaking changes | ||
| var _ conversion.Hub = kv | ||
|
|
||
| // Exit early if no special handling needed | ||
| if kv.Spec.Properties == nil || kv.Spec.Properties.CreateMode == nil { | ||
| return armObj, nil | ||
| } | ||
|
|
||
| // Get resource context | ||
| id, err := ex.getOwner(ctx, kv, resolver) | ||
| if err != nil { | ||
| return nil, eris.Wrap(err, "failed to get and parse resource ID from KeyVault owner") | ||
| } | ||
|
|
||
| // Create Azure SDK client to check for soft-deleted vaults | ||
| vc, err := armkeyvault.NewVaultsClient(id.SubscriptionID, armClient.Creds(), armClient.ClientOptions()) | ||
| if err != nil { | ||
| return nil, eris.Wrap(err, "failed to create new VaultsClient") | ||
| } | ||
|
|
||
| // Determine the correct create mode based on Azure state | ||
| createMode := *kv.Spec.Properties.CreateMode | ||
| if createMode == CreateMode_CreateOrRecover { | ||
| // Check if soft-deleted vault exists and adjust createMode accordingly | ||
| createMode, err = ex.handleCreateOrRecover(ctx, kv, vc, id, log) | ||
| if err != nil { | ||
| return nil, eris.Wrapf(err, "error checking for existence of soft-deleted KeyVault") | ||
| } | ||
| } | ||
|
|
||
| if createMode == CreateMode_PurgeThenCreate { | ||
| // Purge the soft-deleted vault before creating | ||
| err = ex.handlePurgeThenCreate(ctx, kv, vc, log) | ||
| if err != nil { | ||
| return nil, eris.Wrapf(err, "error purging soft-deleted KeyVault") | ||
| } | ||
| createMode = CreateMode_Default | ||
| } | ||
|
|
||
| // Modify the ARM payload with the determined createMode | ||
| spec := armObj.Spec() | ||
| err = reflecthelpers.SetProperty(spec, "Properties.CreateMode", &createMode) | ||
| if err != nil { | ||
| return nil, eris.Wrapf(err, "error setting CreateMode to %s", createMode) | ||
| } | ||
|
|
||
| return armObj, nil | ||
| } | ||
| ``` | ||
|
|
||
| **Key aspects of this example:** | ||
|
|
||
| 1. **Type assertions**: Both for the resource type and hub version | ||
| 2. **Early exit**: Returns original payload if no modification needed | ||
| 3. **External queries**: Checks Azure for soft-deleted vault state | ||
| 4. **Conditional logic**: Different behavior based on createMode and vault state | ||
| 5. **Payload modification**: Uses reflection to safely modify the ARM payload | ||
| 6. **Error handling**: Returns detailed errors that prevent ARM submission | ||
|
|
||
| ## Common Patterns | ||
|
|
||
| ### Pattern 1: Querying Current Azure State | ||
|
|
||
| ```go | ||
| func (ex *ResourceExtension) ModifyARMResource(...) (genruntime.ARMResource, error) { | ||
| // Get the resource ID | ||
| resourceID, hasResourceID := genruntime.GetResourceID(obj) | ||
| if !hasResourceID { | ||
| // Not yet claimed, return unmodified | ||
| return armObj, nil | ||
| } | ||
|
|
||
| // Query current state from Azure | ||
| raw := make(map[string]any) | ||
| _, err = armClient.GetByID(ctx, resourceID, apiVersion, &raw) | ||
| if err != nil { | ||
| // Handle NotFound appropriately | ||
| var responseError *azcore.ResponseError | ||
| if eris.As(err, &responseError) && responseError.StatusCode == http.StatusNotFound { | ||
| return armObj, nil | ||
| } | ||
| return nil, err | ||
| } | ||
|
|
||
| // Use Azure state to modify payload | ||
| // ... | ||
|
|
||
| return armObj, nil | ||
| } | ||
| ``` | ||
|
|
||
| ### Pattern 2: Including Child Resources | ||
|
|
||
| ```go | ||
| func (ex *ResourceExtension) ModifyARMResource(...) (genruntime.ARMResource, error) { | ||
| // Get existing child resources from Azure | ||
| children, err := getRawChildCollection(raw, "subnets") | ||
| if err != nil { | ||
| return nil, eris.Wrapf(err, "failed to get child resources") | ||
| } | ||
|
|
||
| // Merge with desired state in payload | ||
| err = setChildCollection(armObj.Spec(), children, "Subnets") | ||
| if err != nil { | ||
| return nil, eris.Wrapf(err, "failed to set child resources") | ||
| } | ||
|
|
||
| return armObj, nil | ||
| } | ||
| ``` | ||
|
|
||
| ### Pattern 3: Conditional Field Population | ||
|
|
||
| ```go | ||
| func (ex *ResourceExtension) ModifyARMResource(...) (genruntime.ARMResource, error) { | ||
| // Determine value based on runtime conditions | ||
| value, err := ex.computeValue(ctx, obj, armClient) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| // Set the computed value on the ARM resource | ||
| spec := armObj.Spec() | ||
| err = reflecthelpers.SetProperty(spec, "Properties.FieldName", value) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return armObj, nil | ||
| } | ||
| ``` | ||
|
|
||
| ## Testing | ||
|
|
||
| When testing `ARMResourceModifier` extensions: | ||
|
|
||
| 1. **Test all code paths**: Cover each conditional branch | ||
| 2. **Mock ARM responses**: Use envtest with recorded responses | ||
| 3. **Verify payload changes**: Assert that the returned armObj has expected modifications | ||
| 4. **Test error handling**: Ensure errors are properly classified and reported | ||
| 5. **Test edge cases**: No ResourceID, resource not found, nil fields, etc. | ||
|
|
||
| ## Related Extension Points | ||
|
|
||
| - [PreReconciliationChecker]({{< relref "pre-reconciliation-checker" >}}): For validation before ARM operations | ||
| - [PostReconciliationChecker]({{< relref "post-reconciliation-checker" >}}): For validation after ARM operations | ||
| - [ErrorClassifier]({{< relref "error-classifier" >}}): For handling errors from modified requests |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.