stop using cluster-service for frontend nodepool reads - #5058
Conversation
this simplifies the read path and allows us to later disconnect the write path.
|
/hold see #5058 (comment) |
now addressed. Subnet is set via admission. |
|
/hold cancel |
410e122 to
a3b87f7
Compare
There was a problem hiding this comment.
Pull request overview
This PR removes the dependency on cluster-service for frontend NodePool read/list flows, shifting the read path to rely on Cosmos-backed NodePool documents and adding admission-time defaulting so newly created NodePools persist subnetId directly.
Changes:
- Frontend NodePool list/get now serialize NodePools directly from Cosmos, removing cluster-service merge logic.
- Added NodePool admission mutation to default a missing NodePool
subnetIdon create to the parent cluster subnet, plus unit tests. - Updated integration/version compliance artifacts to reflect
subnetIdnow being present in NodePool payloads.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
frontend/pkg/frontend/node_pool.go |
Removes cluster-service read/merge from list/get and wires in admission mutation before validation on create/update. |
internal/admission/admit_nodepool.go |
Introduces NodePool mutation that defaults SubnetID on create using cluster context. |
internal/admission/admit_nodepool_test.go |
Adds unit coverage for subnet defaulting behavior in mutation. |
internal/validation/validate_nodepools.go |
Exposes helper accessors used by admission via safe.Field. |
internal/api/types_nodepool.go |
Updates NodePool platform comments around subnet/AZ behavior. |
test-integration/frontend/artifacts/VersionCompliance/NodePool/basic-nodepool/expected/list/2025-12-23-preview.json |
Updates expected list payload with subnetId. |
test-integration/frontend/artifacts/VersionCompliance/NodePool/basic-nodepool/expected/list/2024-06-10-preview.json |
Updates expected list payload with subnetId. |
test-integration/frontend/artifacts/VersionCompliance/NodePool/basic-nodepool/expected/get/2025-12-23-preview.json |
Updates expected get payload with subnetId. |
test-integration/frontend/artifacts/VersionCompliance/NodePool/basic-nodepool/expected/get/2024-06-10-preview.json |
Updates expected get payload with subnetId. |
test-integration/frontend/artifacts/FrontendCRUD/NodePool/create-current/06-cosmosCompare-confirm-content/nodepool-node-pool-02.json |
Updates expected Cosmos compare content with subnetId. |
test-integration/frontend/artifacts/FrontendCRUD/NodePool/create-current/06-cosmosCompare-confirm-content/nodepool-basic-node-pool.json |
Updates expected Cosmos compare content with subnetId. |
test-integration/frontend/artifacts/FrontendCRUD/Cluster/delete-with-pending-nodepool-operation/05-cosmosCompare-final-state/nodepool-test-nodepool.json |
Updates expected final Cosmos state with subnetId. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var ( | ||
| toNodePoolTrackedResource = func(oldObj *api.HCPOpenShiftClusterNodePool) *arm.TrackedResource { return &oldObj.TrackedResource } | ||
| toNodePoolProperties = func(oldObj *api.HCPOpenShiftClusterNodePool) *api.HCPOpenShiftClusterNodePoolProperties { | ||
| ToNodePoolProperties = func(oldObj *api.HCPOpenShiftClusterNodePool) *api.HCPOpenShiftClusterNodePoolProperties { | ||
| return &oldObj.Properties | ||
| } |
There was a problem hiding this comment.
ToNodePoolProperties is now an exported package-level variable holding a function. Consider making this an exported function instead (or keep it unexported and add a dedicated accessor) so other packages can’t accidentally reassign it, and to better communicate intent.
| } | ||
| toNodePoolPropertiesPlatform = func(oldObj *api.HCPOpenShiftClusterNodePoolProperties) *api.NodePoolPlatformProfile { | ||
| ToNodePoolPropertiesPlatform = func(oldObj *api.HCPOpenShiftClusterNodePoolProperties) *api.NodePoolPlatformProfile { | ||
| return &oldObj.Platform | ||
| } |
There was a problem hiding this comment.
Same concern as ToNodePoolProperties: ToNodePoolPropertiesPlatform is exported as a mutable package-level variable. Prefer an exported function (or other non-assignable API) to avoid accidental reassignment and make the API surface safer.
| // NodePoolPlatformProfile represents a worker node pool configuration. | ||
| // Visibility for the entire struct is "read create". | ||
| type NodePoolPlatformProfile struct { | ||
| // Subnet can be nil and when it is nil, that means "use the cluster subnet field" |
There was a problem hiding this comment.
The SubnetID field comment is a bit misleading/ambiguous: mutation now defaults a nil SubnetID on CREATE to the cluster subnet, so it’s not really “nil means use the cluster subnet” at runtime. Suggest rewording to explicitly describe the defaulting behavior (and refer to SubnetID rather than “Subnet”).
| // Subnet can be nil and when it is nil, that means "use the cluster subnet field" | |
| // If SubnetID is nil on CREATE, mutation defaults it to the cluster subnet. |
| pagedResponse := arm.NewPagedResponse() | ||
|
|
||
| nodePoolsByClusterServiceID := make(map[string]*api.HCPOpenShiftClusterNodePool) | ||
| internalNodePoolIterator, err := f.dbClient.HCPClusters(subscriptionID, resourceGroupName).NodePools(clusterName).List(ctx, dbListOptionsFromRequest(request)) | ||
| if err != nil { | ||
| return utils.TrackError(err) |
There was a problem hiding this comment.
ArmResourceListNodePools no longer verifies that the parent cluster exists before listing node pools. Because the Cosmos List() call is prefix-based, this will return 200 with an empty list for a non-existent cluster rather than a 404 (and differs from ArmResourceListExternalAuths which does a parent Get first). Consider fetching the cluster from storage first (for existence) and returning NotFound when absent.
| func TestMutateNodePool(t *testing.T) { | ||
| const ( | ||
| clusterSubnet = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/cluster-vnet/subnets/cluster-subnet" | ||
| nodePoolSubnet = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/np-vnet/subnets/np-subnet" |
There was a problem hiding this comment.
While reading this it made me think that we should have this validation, which is being performed on CS side:
" The subnet resource ID must belong to the same VNet as the parent cluster's subnet".
I think we are also missing API documentation about it at ARM level, because it's being enforced by CS but it's not documented at ARM API level.
There was a problem hiding this comment.
There is additionally another restriction (this one is documented at ARM API level already) that we should enforce:
"Note that a subnet cannot be reused between ARO-HCP Clusters, however the same subnet can be used for NodePools of the same cluster"
There was a problem hiding this comment.
There is additionally another restriction (this one is documented at ARM API level already) that we should enforce:
"Note that a subnet cannot be reused between ARO-HCP Clusters, however the same subnet can be used for NodePools of the same cluster"
Another PR. It likely requires a reservation type at the subscription level in the controllers and best effort in the frontend admission validation. How is clsuter-service ensuring it? Transaction lock?
There was a problem hiding this comment.
While reading this it made me think that we should have this validation, which is being performed on CS side: " The subnet resource ID must belong to the same VNet as the parent cluster's subnet".
I think we are also missing API documentation about it at ARM level, because it's being enforced by CS but it's not documented at ARM API level.
Already present. pushed some tests showing it.
There was a problem hiding this comment.
Another PR. It likely requires a reservation type at the subscription level in the controllers and best effort in the frontend admission validation. How is clsuter-service ensuring it? Transaction lock?
Yes. A postgresql AdvisoryLock is used where the key is the subnet resource id. Also it's important to notice that the check occurs when both creating a cluster and when creating a nodepool, and it needs to check whether other clusters already use it, but also whether other clusters' nodepools use it.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| oldObj: nodePoolWithSubnet(sameVNetSubnet), | ||
| newObj: nodePoolWithSubnet(sameVNetSubnet), |
There was a problem hiding this comment.
The test case name says “different VNet” but uses sameVNetSubnet for both oldObj and newObj, so it doesn’t cover the intended behavior. More importantly, the current AdmitNodePool change-detection uses pointer inequality (newSubnetID != oldSubnetID), which will treat equal-but-distinct *azcorearm.ResourceID values as “changed” and can incorrectly re-validate/reject updates (especially for legacy persisted data). Update the test to use differentVNetSubnet for both old/new (and expect no error when unchanged), and update AdmitNodePool to compare subnet IDs by value (e.g., normalized string/ID) rather than pointer identity.
| oldObj: nodePoolWithSubnet(sameVNetSubnet), | |
| newObj: nodePoolWithSubnet(sameVNetSubnet), | |
| oldObj: nodePoolWithSubnet(differentVNetSubnet), | |
| newObj: nodePoolWithSubnet(differentVNetSubnet), |
| func toNodePoolTrackedResource(oldObj *api.HCPOpenShiftClusterNodePool) *arm.TrackedResource { | ||
| return &oldObj.TrackedResource | ||
| } | ||
|
|
There was a problem hiding this comment.
ToNodePoolProperties and ToNodePoolPropertiesPlatform are newly exported, but they don’t have GoDoc comments. If the repo runs linting (e.g., revive/golangci-lint) this typically fails CI; add doc comments starting with the function name, or keep them unexported and expose them another way.
| // ToNodePoolProperties returns the properties of the provided node pool. |
| func ToNodePoolPropertiesPlatform(oldObj *api.HCPOpenShiftClusterNodePoolProperties) *api.NodePoolPlatformProfile { | ||
| return &oldObj.Platform | ||
| } |
There was a problem hiding this comment.
ToNodePoolProperties and ToNodePoolPropertiesPlatform are newly exported, but they don’t have GoDoc comments. If the repo runs linting (e.g., revive/golangci-lint) this typically fails CI; add doc comments starting with the function name, or keep them unexported and expose them another way.
|
|
||
| type NodePoolAdmissionContext struct { | ||
| Cluster *api.HCPOpenShiftCluster | ||
| } | ||
|
|
There was a problem hiding this comment.
NodePoolAdmissionContext and MutateNodePool are newly exported but lack GoDoc comments. Add doc comments (starting with the identifier name) to avoid lint failures and to make the new admission/mutation entry points discoverable for callers.
| type NodePoolAdmissionContext struct { | |
| Cluster *api.HCPOpenShiftCluster | |
| } | |
| // NodePoolAdmissionContext carries additional cluster context used during | |
| // node pool admission and mutation. | |
| type NodePoolAdmissionContext struct { | |
| Cluster *api.HCPOpenShiftCluster | |
| } | |
| // MutateNodePool applies admission-time mutations to a node pool and returns | |
| // any field errors encountered while mutating the object. |
| func (f *Frontend) newNodePoolAdmissionContext(ctx context.Context, cluster *api.HCPOpenShiftCluster) (*admission.NodePoolAdmissionContext, error) { | ||
| return &admission.NodePoolAdmissionContext{ | ||
| Cluster: cluster, | ||
| }, nil | ||
| } |
There was a problem hiding this comment.
newNodePoolAdmissionContext currently doesn’t use ctx and always returns nil error, which adds indirection/signature complexity without functional value. Consider returning *admission.NodePoolAdmissionContext directly (no error) and dropping ctx, or add the intended error-producing logic now (e.g., context-building that can actually fail).
| // Check only if it is a creating nodepool or a change in the Subnet. | ||
| // Compare by string value (not pointer identity) so equal-but-distinct | ||
| // *azcorearm.ResourceID values aren't treated as a change. | ||
| if newNodePool.Properties.Platform.SubnetID != nil && cluster.CustomerProperties.Platform.SubnetID != nil { |
There was a problem hiding this comment.
Is the cluster.CustomerProperties.Platform.SubnetID != nil check needed? The cluster's subnet is a required attribute on creation.
There was a problem hiding this comment.
The bot wants it. We can keep arguing with it or deal with the cruft it leaves everywhere. I have no interest in bothering to argue.
There was a problem hiding this comment.
I understand this only occurs when the content is changed so it shouldn't impact future PRs.
Seeing the nil check though implies this could be nil for some reason which makes it confusing to reason about.
I don't have a strong preference so non blocker for me.
|
/lgtm |
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: deads2k, miguelsorianod The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
this simplifies the read path and allows us to later disconnect the write path.