Skip to content

Commit 79f33f1

Browse files
DavidS-ovmactions-user
authored andcommitted
feat(azure): add NetworkLoadBalancerBackendAddressPool adapter (#4393)
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Summary Add a new Azure adapter for Load Balancer Backend Address Pools (ENG-3327). ## Changes - **Client interface**: Created `load-balancer-backend-address-pools-client.go` with `Get` and `NewListPager` methods - **Mock**: Generated mock client for unit testing - **Adapter implementation**: `network-load-balancer-backend-address-pool.go` implementing `SearchableWrapper` with: - `Get` method requiring `loadBalancerName` and `backendAddressPoolName` query parts - `Search` and `SearchStream` methods requiring `loadBalancerName` query part - Health status mapping from provisioning state - Input validation for empty query parts - **Linked item queries**: - Parent: NetworkLoadBalancer - VirtualNetwork (pool and address level) - Subnet (from backend addresses) - NetworkInterface (from backend IP configurations) - InboundNatRule, LoadBalancingRule, OutboundRule references - FrontendIPConfiguration (from regional LB references) - stdlib NetworkIP (from backend address IP addresses) - **Registration**: Added to `adapters.go` in both active and placeholder blocks - **Unit tests**: Comprehensive tests including StaticTests for linked queries - **Integration test**: Setup/Run/Teardown structure with Get, Search, VerifyLinkedItems, and VerifyItemAttributes tests ## Self-Review Checklist - [x] **IAMPermissions**: Present, references `Microsoft.Network/loadBalancers/backendAddressPools/read` - [x] **PredefinedRole**: Present, uses `Reader` - [x] **LinkedItemQueries**: 10 link types verified (parent LB, VNets, subnets, NICs, NAT rules, LB rules, outbound rules, frontend IPs, IP addresses). IP links included. - [x] **PotentialLinks**: 9 types listed, matches LinkedItemQueries - [x] **Unit tests**: All passing (Get, Search, SearchStream, StaticTests, ErrorHandling, empty validation) - [x] **Integration test**: Present, follows Setup/Run/Teardown structure All checklist items passed. Ready for review. <!-- CURSOR_AGENT_PR_BODY_END --> Linear Issue: [ENG-3327](https://linear.app/overmind/issue/ENG-3327/create-azure-adapter-networkloadbalancerbackendaddresspool) <div><a href="https://cursor.com/agents/bc-6a1336c3-9cda-48d4-a15a-7a3d815ee9eb"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-6a1336c3-9cda-48d4-a15a-7a3d815ee9eb"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div> Passing integration test : <img width="744" height="992" alt="image" src="https://github.com/user-attachments/assets/7e6806e1-fe22-4786-b4dc-7ded89f001f8" /> GitOrigin-RevId: 4c6f3ae3a9e4029379959b970b67009d945c98cf
1 parent 5445b64 commit 79f33f1

14 files changed

Lines changed: 1638 additions & 48 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package clients
2+
3+
import (
4+
"context"
5+
6+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9"
7+
)
8+
9+
//go:generate mockgen -destination=../shared/mocks/mock_load_balancer_backend_address_pools_client.go -package=mocks -source=load-balancer-backend-address-pools-client.go
10+
11+
// LoadBalancerBackendAddressPoolsPager is a type alias for the generic Pager interface.
12+
type LoadBalancerBackendAddressPoolsPager = Pager[armnetwork.LoadBalancerBackendAddressPoolsClientListResponse]
13+
14+
// LoadBalancerBackendAddressPoolsClient is an interface for interacting with Azure load balancer backend address pools.
15+
type LoadBalancerBackendAddressPoolsClient interface {
16+
Get(ctx context.Context, resourceGroupName string, loadBalancerName string, backendAddressPoolName string) (armnetwork.LoadBalancerBackendAddressPoolsClientGetResponse, error)
17+
NewListPager(resourceGroupName string, loadBalancerName string) LoadBalancerBackendAddressPoolsPager
18+
}
19+
20+
type loadBalancerBackendAddressPoolsClient struct {
21+
client *armnetwork.LoadBalancerBackendAddressPoolsClient
22+
}
23+
24+
func (a *loadBalancerBackendAddressPoolsClient) Get(ctx context.Context, resourceGroupName string, loadBalancerName string, backendAddressPoolName string) (armnetwork.LoadBalancerBackendAddressPoolsClientGetResponse, error) {
25+
return a.client.Get(ctx, resourceGroupName, loadBalancerName, backendAddressPoolName, nil)
26+
}
27+
28+
func (a *loadBalancerBackendAddressPoolsClient) NewListPager(resourceGroupName string, loadBalancerName string) LoadBalancerBackendAddressPoolsPager {
29+
return a.client.NewListPager(resourceGroupName, loadBalancerName, nil)
30+
}
31+
32+
// NewLoadBalancerBackendAddressPoolsClient creates a new LoadBalancerBackendAddressPoolsClient from the Azure SDK client.
33+
func NewLoadBalancerBackendAddressPoolsClient(client *armnetwork.LoadBalancerBackendAddressPoolsClient) LoadBalancerBackendAddressPoolsClient {
34+
return &loadBalancerBackendAddressPoolsClient{client: client}
35+
}

sources/azure/integration-tests/authorization-role-assignment_test.go

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,9 @@ func TestAuthorizationRoleAssignmentIntegration(t *testing.T) {
6868
t.Fatalf("Failed to get Reader role definition ID: %v", err)
6969
}
7070

71-
// Generate unique role assignment name (GUID)
72-
roleAssignmentName := uuid.New().String()
71+
// Deterministic role assignment name so re-runs reuse the same assignment ID
72+
// instead of conflicting with a prior run's different UUID for the same principal+role combo
73+
roleAssignmentName := uuid.NewSHA1(uuid.NameSpaceURL, []byte(principalID+readerRoleDefinitionID+integrationTestResourceGroup)).String()
7374
azureScope := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s", subscriptionID, integrationTestResourceGroup)
7475
setupCompleted := false
7576

@@ -83,10 +84,11 @@ func TestAuthorizationRoleAssignmentIntegration(t *testing.T) {
8384
}
8485

8586
// Create role assignment at resource group scope
86-
err = createRoleAssignment(ctx, roleAssignmentsClient, azureScope, roleAssignmentName, principalID, readerRoleDefinitionID)
87-
if err != nil {
88-
t.Fatalf("Failed to create role assignment: %v", err)
87+
actualName, createErr := createRoleAssignment(ctx, roleAssignmentsClient, azureScope, roleAssignmentName, principalID, readerRoleDefinitionID)
88+
if createErr != nil {
89+
t.Fatalf("Failed to create role assignment: %v", createErr)
8990
}
91+
roleAssignmentName = actualName
9092
err = waitForRoleAssignmentAvailable(ctx, roleAssignmentsClient, azureScope, roleAssignmentName)
9193
if err != nil {
9294
t.Fatalf("Failed waiting for role assignment to be available: %v", err)
@@ -365,29 +367,22 @@ func getReaderRoleDefinitionID(ctx context.Context, client *armauthorization.Rol
365367
return "", fmt.Errorf("Reader role definition not found")
366368
}
367369

368-
// createRoleAssignment creates an Azure role assignment (idempotent)
369-
func createRoleAssignment(ctx context.Context, client *armauthorization.RoleAssignmentsClient, scope, roleAssignmentName, principalID, roleDefinitionID string) error {
370+
// createRoleAssignment creates an Azure role assignment (idempotent).
371+
// Returns the actual assignment name used (may differ from input if a prior run
372+
// created the same principal+role combo under a different UUID).
373+
func createRoleAssignment(ctx context.Context, client *armauthorization.RoleAssignmentsClient, scope, roleAssignmentName, principalID, roleDefinitionID string) (string, error) {
370374
return createRoleAssignmentWithRemediation(ctx, client, scope, roleAssignmentName, principalID, roleDefinitionID, 0)
371375
}
372376

373-
func createRoleAssignmentWithRemediation(ctx context.Context, client *armauthorization.RoleAssignmentsClient, scope, roleAssignmentName, principalID, roleDefinitionID string, remediationAttempt int) error {
374-
// Check if role assignment already exists
377+
func createRoleAssignmentWithRemediation(ctx context.Context, client *armauthorization.RoleAssignmentsClient, scope, roleAssignmentName, principalID, roleDefinitionID string, remediationAttempt int) (string, error) {
375378
_, err := client.Get(ctx, scope, roleAssignmentName, nil)
376379
if err == nil {
377380
log.Printf("Role assignment %s already exists, skipping creation", roleAssignmentName)
378-
return nil
381+
return roleAssignmentName, nil
379382
}
380383

381-
// Create the role assignment
382-
// Note: We need to get the principal ID from the current user or a service principal
383-
// For integration tests, we'll use Azure CLI to get the current user's object ID
384-
// This requires running: az ad signed-in-user show --query id -o tsv
385-
// Or using Graph API
386-
387-
// For now, let's try to create it and handle the error if principal ID is needed
388-
// Actually, we should get the principal ID before calling this function
389384
if principalID == "" {
390-
return fmt.Errorf("principal ID is required to create role assignment")
385+
return "", fmt.Errorf("principal ID is required to create role assignment")
391386
}
392387

393388
parameters := armauthorization.RoleAssignmentCreateParameters{
@@ -402,39 +397,68 @@ func createRoleAssignmentWithRemediation(ctx context.Context, client *armauthori
402397
var respErr *azcore.ResponseError
403398
if errors.As(err, &respErr) {
404399
if respErr.StatusCode == http.StatusConflict {
400+
if strings.Contains(respErr.Error(), "RoleAssignmentExists") {
401+
existingID := extractExistingRoleAssignmentID(respErr.Error())
402+
if existingID != "" {
403+
log.Printf("Role assignment for this principal+role already exists at scope %s with ID %s, reusing", scope, existingID)
404+
return existingID, nil
405+
}
406+
log.Printf("Role assignment for this principal+role already exists at scope %s, treating as success", scope)
407+
return roleAssignmentName, nil
408+
}
405409
existing, getErr := client.Get(ctx, scope, roleAssignmentName, nil)
406410
if getErr == nil && existing.RoleAssignment.ID != nil && *existing.RoleAssignment.ID != "" {
407411
log.Printf("Role assignment %s already exists (conflict), verified readable, skipping creation", roleAssignmentName)
408-
return nil
412+
return roleAssignmentName, nil
409413
}
410414
var getRespErr *azcore.ResponseError
411415
if errors.As(getErr, &getRespErr) && getRespErr.StatusCode == http.StatusNotFound {
412416
if remediationAttempt >= 1 {
413-
return fmt.Errorf("role assignment %s still in ghost conflict state after remediation (scope=%s): %w", roleAssignmentName, scope, err)
417+
return "", fmt.Errorf("role assignment %s still in ghost conflict state after remediation (scope=%s): %w", roleAssignmentName, scope, err)
414418
}
415419
log.Printf("Detected ghost role-assignment conflict for %s at %s, attempting automatic remediation", roleAssignmentName, scope)
416420
if deleteErr := deleteRoleAssignment(ctx, client, scope, roleAssignmentName); deleteErr != nil {
417-
return fmt.Errorf("failed to remediate ghost role assignment %s before retry: %w", roleAssignmentName, deleteErr)
421+
return "", fmt.Errorf("failed to remediate ghost role assignment %s before retry: %w", roleAssignmentName, deleteErr)
418422
}
419423
time.Sleep(5 * time.Second)
420424
return createRoleAssignmentWithRemediation(ctx, client, scope, roleAssignmentName, principalID, roleDefinitionID, remediationAttempt+1)
421425
}
422-
return fmt.Errorf("role assignment conflict for %s and failed to verify existing role assignment: %w", roleAssignmentName, getErr)
426+
return "", fmt.Errorf("role assignment conflict for %s and failed to verify existing role assignment: %w", roleAssignmentName, getErr)
423427
}
424428
if respErr.StatusCode == http.StatusForbidden {
425-
return fmt.Errorf("insufficient permissions to create role assignment: %w", err)
429+
return "", fmt.Errorf("insufficient permissions to create role assignment: %w", err)
426430
}
427431
}
428-
return fmt.Errorf("failed to create role assignment: %w", err)
432+
return "", fmt.Errorf("failed to create role assignment: %w", err)
429433
}
430434

431-
// Verify the role assignment was created successfully
432435
if resp.RoleAssignment.ID == nil {
433-
return fmt.Errorf("role assignment created but ID is unknown")
436+
return "", fmt.Errorf("role assignment created but ID is unknown")
434437
}
435438

436439
log.Printf("Role assignment %s created successfully at scope %s", roleAssignmentName, scope)
437-
return nil
440+
return roleAssignmentName, nil
441+
}
442+
443+
// extractExistingRoleAssignmentID parses the existing assignment ID from the
444+
// RoleAssignmentExists error message (format: "...The ID of the existing role
445+
// assignment is <hex-id-no-dashes>.").
446+
func extractExistingRoleAssignmentID(errMsg string) string {
447+
const marker = "The ID of the existing role assignment is "
448+
_, after, ok := strings.Cut(errMsg, marker)
449+
if !ok {
450+
return ""
451+
}
452+
rest := after
453+
if dotIdx := strings.Index(rest, "."); dotIdx > 0 {
454+
rest = rest[:dotIdx]
455+
}
456+
rest = strings.TrimSpace(rest)
457+
if len(rest) != 32 {
458+
return rest
459+
}
460+
// Convert 32-char hex to UUID format (8-4-4-4-12)
461+
return rest[:8] + "-" + rest[8:12] + "-" + rest[12:16] + "-" + rest[16:20] + "-" + rest[20:]
438462
}
439463

440464
// deleteRoleAssignment deletes an Azure role assignment

sources/azure/integration-tests/compute-availability-set_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -385,11 +385,12 @@ func createAvailabilitySet(ctx context.Context, client *armcompute.AvailabilityS
385385
// Create the availability set
386386
resp, err := client.CreateOrUpdate(ctx, resourceGroupName, avSetName, armcompute.AvailabilitySet{
387387
Location: new(location),
388+
SKU: &armcompute.SKU{
389+
Name: new("Aligned"),
390+
},
388391
Properties: &armcompute.AvailabilitySetProperties{
389392
PlatformFaultDomainCount: new(int32(2)),
390393
PlatformUpdateDomainCount: new(int32(2)),
391-
ProximityPlacementGroup: nil, // Optional - not setting for this test
392-
VirtualMachines: nil, // Will be populated when VMs are added
393394
},
394395
Tags: map[string]*string{
395396
"purpose": new("overmind-integration-tests"),
@@ -579,14 +580,13 @@ func createVirtualMachineWithAvailabilitySetWithRemediation(ctx context.Context,
579580
Location: new(location),
580581
Properties: &armcompute.VirtualMachineProperties{
581582
HardwareProfile: &armcompute.HardwareProfile{
582-
// Use Standard_D2ps_v5 - ARM-based VM with good availability in westus2
583-
VMSize: new(armcompute.VirtualMachineSizeTypes("Standard_D2ps_v5")),
583+
VMSize: new(armcompute.VirtualMachineSizeTypes("Standard_D2s_v3")),
584584
},
585585
StorageProfile: &armcompute.StorageProfile{
586586
ImageReference: &armcompute.ImageReference{
587587
Publisher: new("Canonical"),
588588
Offer: new("0001-com-ubuntu-server-jammy"),
589-
SKU: new("22_04-lts-arm64"), // ARM64 image for ARM-based VM
589+
SKU: new("22_04-lts"),
590590
Version: new("latest"),
591591
},
592592
OSDisk: &armcompute.OSDisk{

sources/azure/integration-tests/compute-virtual-machine-extension_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -444,14 +444,13 @@ func createVirtualMachineForExtensionWithRemediation(ctx context.Context, client
444444
Location: new(location),
445445
Properties: &armcompute.VirtualMachineProperties{
446446
HardwareProfile: &armcompute.HardwareProfile{
447-
// Use Standard_D2ps_v5 - ARM-based VM with good availability in westus2
448-
VMSize: new(armcompute.VirtualMachineSizeTypes("Standard_D2ps_v5")),
447+
VMSize: new(armcompute.VirtualMachineSizeTypes("Standard_D2s_v3")),
449448
},
450449
StorageProfile: &armcompute.StorageProfile{
451450
ImageReference: &armcompute.ImageReference{
452451
Publisher: new("Canonical"),
453452
Offer: new("0001-com-ubuntu-server-jammy"),
454-
SKU: new("22_04-lts-arm64"), // ARM64 image for ARM-based VM
453+
SKU: new("22_04-lts"),
455454
Version: new("latest"),
456455
},
457456
OSDisk: &armcompute.OSDisk{

sources/azure/integration-tests/compute-virtual-machine-run-command_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -444,14 +444,13 @@ func createVirtualMachineForRunCommandWithRemediation(ctx context.Context, clien
444444
Location: new(location),
445445
Properties: &armcompute.VirtualMachineProperties{
446446
HardwareProfile: &armcompute.HardwareProfile{
447-
// Use Standard_D2ps_v5 - ARM-based VM with good availability in westus2
448-
VMSize: new(armcompute.VirtualMachineSizeTypes("Standard_D2ps_v5")),
447+
VMSize: new(armcompute.VirtualMachineSizeTypes("Standard_D2s_v3")),
449448
},
450449
StorageProfile: &armcompute.StorageProfile{
451450
ImageReference: &armcompute.ImageReference{
452451
Publisher: new("Canonical"),
453452
Offer: new("0001-com-ubuntu-server-jammy"),
454-
SKU: new("22_04-lts-arm64"), // ARM64 image for ARM-based VM
453+
SKU: new("22_04-lts"),
455454
Version: new("latest"),
456455
},
457456
OSDisk: &armcompute.OSDisk{

sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ func createVirtualMachineScaleSet(ctx context.Context, client *armcompute.Virtua
409409
poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vmssName, armcompute.VirtualMachineScaleSet{
410410
Location: new(location),
411411
SKU: &armcompute.SKU{
412-
Name: new("Standard_B1s"), // Burstable B-series VM - cheaper and more widely available
412+
Name: new("Standard_D2s_v3"),
413413
Tier: new("Standard"),
414414
Capacity: new(int64(1)), // Start with 1 instance for testing
415415
},
@@ -489,7 +489,7 @@ func createVirtualMachineScaleSet(ctx context.Context, client *armcompute.Virtua
489489
retryPoller, retryErr := client.BeginCreateOrUpdate(ctx, resourceGroupName, vmssName, armcompute.VirtualMachineScaleSet{
490490
Location: new(location),
491491
SKU: &armcompute.SKU{
492-
Name: new("Standard_B1s"),
492+
Name: new("Standard_D2s_v3"),
493493
Tier: new("Standard"),
494494
Capacity: new(int64(1)),
495495
},

sources/azure/integration-tests/compute-virtual-machine_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -413,14 +413,13 @@ func createVirtualMachineWithRemediation(ctx context.Context, client *armcompute
413413
Location: new(location),
414414
Properties: &armcompute.VirtualMachineProperties{
415415
HardwareProfile: &armcompute.HardwareProfile{
416-
// Use Standard_D2ps_v5 - ARM-based VM with good availability in westus2
417-
VMSize: new(armcompute.VirtualMachineSizeTypes("Standard_D2ps_v5")),
416+
VMSize: new(armcompute.VirtualMachineSizeTypes("Standard_D2s_v3")),
418417
},
419418
StorageProfile: &armcompute.StorageProfile{
420419
ImageReference: &armcompute.ImageReference{
421420
Publisher: new("Canonical"),
422421
Offer: new("0001-com-ubuntu-server-jammy"),
423-
SKU: new("22_04-lts-arm64"), // ARM64 image for ARM-based VM
422+
SKU: new("22_04-lts"),
424423
Version: new("latest"),
425424
},
426425
OSDisk: &armcompute.OSDisk{

sources/azure/integration-tests/helpers_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ var invalidRunIDSanitizer = regexp.MustCompile(`[^a-z0-9-]+`)
2525
// optionally scoped by AZURE_INTEGRATION_TEST_RUN_ID for parallel runs.
2626
//
2727
// Example:
28-
// AZURE_INTEGRATION_TEST_RUN_ID=agent-42
29-
// => overmind-integration-tests-agent-42
28+
//
29+
// AZURE_INTEGRATION_TEST_RUN_ID=agent-42
30+
// => overmind-integration-tests-agent-42
3031
func resolveIntegrationTestResourceGroup() string {
3132
runID := normalizeIntegrationTestRunID(os.Getenv("AZURE_INTEGRATION_TEST_RUN_ID"))
3233
if runID == "" {

sources/azure/integration-tests/network-flow-log_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@ import (
66
"fmt"
77
"net/http"
88
"os"
9+
"strings"
910
"testing"
1011
"time"
1112

12-
"strings"
13-
1413
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
1514
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9"
1615
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2"

0 commit comments

Comments
 (0)