Skip to content

CORS-3900: Add IPv6 frontend IP configurations for dual-stack#10328

Open
rna-afk wants to merge 1 commit intoopenshift:mainfrom
rna-afk:azure_dualstack_add_frontend
Open

CORS-3900: Add IPv6 frontend IP configurations for dual-stack#10328
rna-afk wants to merge 1 commit intoopenshift:mainfrom
rna-afk:azure_dualstack_add_frontend

Conversation

@rna-afk
Copy link
Contributor

@rna-afk rna-afk commented Feb 23, 2026

Adding IPv6 public IP creation and frontend IP configurations on external load balancers and adding IPv6 frontend IP to CAPZ apiServerLB manifest.

Also adding IPv6 NAT rule for bootstrap SSH access and updating NAT rules to the correct IP version on the bootstrap NIC.

Summary by CodeRabbit

  • New Features
    • Added IPv6 dual-stack support for Azure infrastructure, enabling IPv6 frontend allocation on internal and external load balancers when dual-stack is enabled.
    • Enhanced public IP allocation and management with IPv6 support.
    • Improved load balancer configuration with dual-stack IPv4/IPv6 frontend handling for API and internal load balancers.

@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Feb 23, 2026
@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Feb 23, 2026

@rna-afk: This pull request references CORS-3900 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

Adding IPv6 public IP creation and frontend IP configurations on external load balancers and adding IPv6 frontend IP to CAPZ apiServerLB manifest.

Also adding IPv6 NAT rule for bootstrap SSH access and updating NAT rules to the correct IP version on the bootstrap NIC.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

if virtualNetwork != nil {
virtualNetworkID = *virtualNetwork.ID
}
lbip, err := getNextAvailableIPForLoadBalancer(ctx, installConfig, lbip)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point for dualstack, apiServerLB.FrontendIPs would contain both ipv4 lbip and ipv6 IP addresses. But lbip ip address passed in here will contain only the IPv4 address. Can we update getNextAvailableIPForLoadBalancer() as part of this PR to handle ipv6 subnets in the field platform.Azure.Subnets?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wanted to try the dynamic allocation so we don't get the reserved IP problem. Was hoping this will work so I can fix the ipv4 address too to dynamic allocation. Need to test with John's work to see if it really works.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like John added it in his PR #10319

}

func createAPILoadBalancer(ctx context.Context, pip *armnetwork.PublicIPAddress, in *lbInput) (*armnetwork.LoadBalancer, error) {
func createAPILoadBalancer(ctx context.Context, pip, pipv6 *armnetwork.PublicIPAddress, in *lbInput) (*armnetwork.LoadBalancer, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pipv6 is not used in this method. Updates to this method are still being worked on?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed in the next backend pool PR. I had some errors coming in so added it here too.

@rna-afk rna-afk force-pushed the azure_dualstack_add_frontend branch 5 times, most recently from 55bb8c4 to 71f85d1 Compare March 4, 2026 21:22
Adding IPv6 public IP creation and frontend IP configurations on
external load balancers and adding IPv6 frontend IP to CAPZ
apiServerLB manifest.
@rna-afk rna-afk force-pushed the azure_dualstack_add_frontend branch 2 times, most recently from 4f9fa53 to 7b6dffa Compare March 9, 2026 14:32
@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

Walkthrough

Adds IPv6/dual-stack networking support to Azure infrastructure provisioning by introducing IPv6 public IP creation, dual-stack load balancer frontend configurations, and IPv6 NAT rule association logic, guarded behind dual-stack enablement checks throughout the provider and network layers.

Changes

Cohort / File(s) Summary
Cluster Manifest Configuration
pkg/asset/manifests/azure/cluster.go
Appends IPv6 frontend configuration (name suffixed with -v6) to internal load balancer when dual-stack is enabled, allowing dynamic IPv6 allocation.
Provider Dual-Stack Initialization
pkg/infrastructure/azure/azure.go
Adds publicLBIPv6 field to Provider struct and isDualstack field to lbInput; wires IPv6 public IP creation and assignment into InfraReady and PostProvision flows with conditional logic based on DualStackEnabled().
Load Balancer and Network Operations
pkg/infrastructure/azure/network.go
Extends load balancer creation/update functions (API and internal LB) to handle dual-stack frontends; updates public IP handling to respect IPv4/IPv6 versions; enhances NAT rule association to detect and attach IPv6 rules to correct IP configurations; adds dedicated IPv6 outbound LB updater function.

Sequence Diagram(s)

sequenceDiagram
    participant Provider as Provider
    participant InfraReady as InfraReady Flow
    participant PublicIP as Public IP Creation
    participant LBOps as Load Balancer Operations
    participant NAT as NAT Rule Association

    Provider->>InfraReady: Check DualStackEnabled()
    alt Dual-Stack Enabled
        InfraReady->>PublicIP: Create IPv6 Public IP
        PublicIP-->>Provider: Return IPv6 address
        Provider->>LBOps: Pass IPv6 IP reference
        LBOps->>LBOps: Add IPv6 frontend config
        LBOps->>LBOps: Create IPv6 backend pool
        LBOps->>LBOps: Add IPv6 load balancing rules
        LBOps-->>Provider: Updated LB with dual-stack
        Provider->>NAT: Update internal LB post-reconciliation
        NAT->>NAT: addIPv6InternalLBFrontend
        NAT-->>Provider: IPv6 frontend appended
        Provider->>NAT: Associate IPv6 NAT rules
        NAT->>NAT: Detect -v6 in frontendIPConfigID
        NAT->>NAT: Match to IPv6 IP config
        NAT-->>Provider: NAT rules attached
    else IPv4-Only
        InfraReady->>LBOps: Use existing IPv4 logic
        LBOps-->>Provider: IPv4-only LB
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding IPv6 frontend IP configurations for dual-stack support, which aligns with the primary changes across all modified files.
Stable And Deterministic Test Names ✅ Passed The pull request does not include any Ginkgo test files or test definitions, only infrastructure configuration changes for Azure dual-stack support.
Test Structure And Quality ✅ Passed This pull request does not include or modify any Ginkgo test code. The changes are exclusively in production code files. Since the custom check evaluates test structure and quality for Ginkgo tests, and no test files are modified in this PR, the check does not apply and therefore passes by default.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.5.0)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Mar 9, 2026

@rna-afk: This pull request references CORS-3900 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

Adding IPv6 public IP creation and frontend IP configurations on external load balancers and adding IPv6 frontend IP to CAPZ apiServerLB manifest.

Also adding IPv6 NAT rule for bootstrap SSH access and updating NAT rules to the correct IP version on the bootstrap NIC.

Summary by CodeRabbit

  • New Features
  • Added IPv6 dual-stack support for Azure infrastructure, enabling IPv6 frontend allocation on internal and external load balancers when dual-stack is enabled.
  • Enhanced public IP allocation and management with IPv6 support.
  • Improved load balancer configuration with dual-stack IPv4/IPv6 frontend handling for API and internal load balancers.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci
Copy link
Contributor

openshift-ci bot commented Mar 9, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign rna-afk for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/infrastructure/azure/azure.go`:
- Around line 622-645: The code creates a public IPv6 and updates the outbound
IPv6 LB but never provisions the IPv6 SSH inbound NAT rule; after
updateOutboundIPv6LoadBalancer(...) use addInboundNatRuleToLoadBalancer(...) to
create the SSH inbound NAT rule on outboundLBName using publicIPv6outbound (pass
the load balancer client from p.NetworkClientFactory.NewLoadBalancersClient(),
the resource group and infraID), then call
associateInboundNatRuleToInterface(...) (using
p.NetworkClientFactory.NewInterfacesClient(), the created NAT rule name,
resource group and the relevant interface(s)/VMSS NICs for bootstrap) so the
IPv6 SSH port is actually forwarded (refer to addInboundNatRuleToLoadBalancer
and associateInboundNatRuleToInterface for exact parameter ordering and naming).

In `@pkg/infrastructure/azure/network.go`:
- Around line 144-179: The dual-stack path adds an IPv6 frontend but never
creates a matching IPv6 load-balancing rule, so the API remains IPv4-only;
update both createAPILoadBalancer and
updateOutboundLoadBalancerToAPILoadBalancer to add a second
armnetwork.LoadBalancingRule (e.g., name "api-v6") mirroring the "api-v4" rule
but referencing the IPv6 frontend (frontendIPv6Name), using the same
ports/probe/backend pool and appropriate IPv6 settings; ensure the new rule is
appended to LoadBalancingRules alongside the existing "api-v4" entry so the IPv6
public IP forwards 6443 traffic too.
- Around line 533-557: The loop that attaches inboundNatRule to
bootstrapInterface.Properties.IPConfigurations can fall through without
attaching (due to the IPv4/IPv6 filter) but still return success; add a boolean
flag (e.g., attached) initialized false before the loop, set it true when you
append inboundNatRule to an IP config, and after the loop check the flag and
return an error if attached is false so the caller knows the NAT rule was not
attached to the bootstrap NIC (use the existing identifiers bootstrapInterface,
inboundNatRule, and in.frontendIPConfigID to locate the logic).
- Around line 373-397: The current logic always appends a new IPv6 frontend
config named frontendIPv6Name causing duplicates; instead, scan
existingFrontEndIPConfig (or intLB.Properties.FrontendIPConfigurations) for an
entry with Name == frontendIPv6Name and if found, update that entry's Properties
(set PrivateIPAllocationMethod, PrivateIPAddressVersion and Subnet.ID if
subnetID != "") to ensure it has the desired IPv6 settings; only if no matching
Name is found, append the new frontendIPv6. Use the existing variables
frontendIPv6Name, frontendIPv6 (or create a properties struct),
existingFrontEndIPConfig and intLB.Properties.FrontendIPConfigurations to locate
and mutate or append the config so reconciliation is idempotent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6840915f-3259-48b4-b67f-5b83003c702e

📥 Commits

Reviewing files that changed from the base of the PR and between 4006847 and 7b6dffa.

📒 Files selected for processing (3)
  • pkg/asset/manifests/azure/cluster.go
  • pkg/infrastructure/azure/azure.go
  • pkg/infrastructure/azure/network.go

Comment on lines +622 to +645
// For dual-stack, create IPv6 inbound NAT rule for SSH access to bootstrap.
if in.InstallConfig.Config.Azure.IPFamily.DualStackEnabled() {
publicIPv6outbound, err := createPublicIP(ctx, &pipInput{
name: fmt.Sprintf("%s-pip-v6-outbound-lb", in.InfraID),
infraID: in.InfraID,
region: in.InstallConfig.Config.Azure.Region,
resourceGroup: p.ResourceGroupName,
pipClient: p.NetworkClientFactory.NewPublicIPAddressesClient(),
tags: p.Tags,
ipversion: armnetwork.IPVersionIPv6,
})
if err != nil {
return fmt.Errorf("failed to create public ipv6 for outbound ipv6 lb: %w", err)
}
logrus.Debugf("created public ipv6 for outbound ipv6 lb: %s", *publicIPv6outbound.ID)

// Update the outbound node IPv6 load balancer.
outboundLBName := fmt.Sprintf("%s-ipv6-outbound-node-lb", in.InfraID)
err = updateOutboundIPv6LoadBalancer(ctx, publicIPv6outbound, p.NetworkClientFactory.NewLoadBalancersClient(), p.ResourceGroupName, outboundLBName, in.InfraID)
if err != nil {
return fmt.Errorf("failed to set public ipv6 to outbound ipv6 lb: %w", err)
}
logrus.Debugf("updated outbound ipv6 lb %s with public ipv6: %s", outboundLBName, *publicIPv6outbound.ID)
}
Copy link

@coderabbitai coderabbitai bot Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This block never creates the IPv6 SSH forward it describes.

It allocates a public IPv6 and patches the outbound node LB, but it never calls addInboundNatRuleToLoadBalancer or associateInboundNatRuleToInterface. Dual-stack still only gets the IPv4 *_ssh_in path.

💡 Suggested change
 		// For dual-stack, create IPv6 inbound NAT rule for SSH access to bootstrap.
 		if in.InstallConfig.Config.Azure.IPFamily.DualStackEnabled() {
+			frontendIPv6ConfigID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/frontendIPConfigurations/%s",
+				subscriptionID,
+				p.ResourceGroupName,
+				loadBalancerName,
+				"public-lb-ip-v6",
+			)
+			sshRuleNameV6 := fmt.Sprintf("%s_ssh_in_v6", in.InfraID)
+			inboundNatRuleV6, err := addInboundNatRuleToLoadBalancer(ctx, &inboundNatRuleInput{
+				resourceGroupName:    p.ResourceGroupName,
+				loadBalancerName:     loadBalancerName,
+				frontendIPConfigID:   frontendIPv6ConfigID,
+				inboundNatRuleName:   sshRuleNameV6,
+				inboundNatRulePort:   22,
+				networkClientFactory: p.NetworkClientFactory,
+			})
+			if err != nil {
+				return fmt.Errorf("failed to create inbound ipv6 nat rule: %w", err)
+			}
+			_, err = associateInboundNatRuleToInterface(ctx, &inboundNatRuleInput{
+				resourceGroupName:    p.ResourceGroupName,
+				loadBalancerName:     loadBalancerName,
+				bootstrapNicName:     fmt.Sprintf("%s-bootstrap-nic", in.InfraID),
+				frontendIPConfigID:   frontendIPv6ConfigID,
+				inboundNatRuleID:     *inboundNatRuleV6.ID,
+				inboundNatRuleName:   sshRuleNameV6,
+				inboundNatRulePort:   22,
+				networkClientFactory: p.NetworkClientFactory,
+			})
+			if err != nil {
+				return fmt.Errorf("failed to associate inbound ipv6 nat rule to interface: %w", err)
+			}
+
 			publicIPv6outbound, err := createPublicIP(ctx, &pipInput{
 				name:          fmt.Sprintf("%s-pip-v6-outbound-lb", in.InfraID),
 				infraID:       in.InfraID,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/infrastructure/azure/azure.go` around lines 622 - 645, The code creates a
public IPv6 and updates the outbound IPv6 LB but never provisions the IPv6 SSH
inbound NAT rule; after updateOutboundIPv6LoadBalancer(...) use
addInboundNatRuleToLoadBalancer(...) to create the SSH inbound NAT rule on
outboundLBName using publicIPv6outbound (pass the load balancer client from
p.NetworkClientFactory.NewLoadBalancersClient(), the resource group and
infraID), then call associateInboundNatRuleToInterface(...) (using
p.NetworkClientFactory.NewInterfacesClient(), the created NAT rule name,
resource group and the relevant interface(s)/VMSS NICs for bootstrap) so the
IPv6 SSH port is actually forwarded (refer to addInboundNatRuleToLoadBalancer
and associateInboundNatRuleToInterface for exact parameter ordering and naming).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

called in a later PR

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rna-afk, understood — thanks for the heads-up! Would you like me to open a follow-up GitHub issue to track the missing IPv6 SSH inbound NAT rule creation and association for the dual-stack bootstrap path, so it isn't lost before that later PR lands?

Comment on lines +144 to +179
LoadBalancingRules: []*armnetwork.LoadBalancingRule{
{
Name: to.Ptr("api-v4"),
Properties: &armnetwork.LoadBalancingRulePropertiesFormat{
Protocol: to.Ptr(armnetwork.TransportProtocolTCP),
FrontendPort: to.Ptr[int32](6443),
BackendPort: to.Ptr[int32](6443),
IdleTimeoutInMinutes: to.Ptr[int32](30),
EnableFloatingIP: to.Ptr(false),
LoadDistribution: to.Ptr(armnetwork.LoadDistributionDefault),
FrontendIPConfiguration: &armnetwork.SubResource{
ID: to.Ptr(fmt.Sprintf("/%s/%s/frontendIPConfigurations/%s", in.idPrefix, in.loadBalancerName, *frontendIPv4Name)),
},
BackendAddressPool: &armnetwork.SubResource{
ID: to.Ptr(fmt.Sprintf("/%s/%s/backendAddressPools/%s", in.idPrefix, in.loadBalancerName, in.backendAddressPoolName)),
},
Probe: &armnetwork.SubResource{
ID: to.Ptr(fmt.Sprintf("/%s/%s/probes/%s", in.idPrefix, in.loadBalancerName, probeName)),
},
},
},
},
Tags: in.tags,
}, nil)
},
Tags: in.tags,
}
if in.isDualstack {
loadBalancer.Properties.FrontendIPConfigurations = append(loadBalancer.Properties.FrontendIPConfigurations,
&armnetwork.FrontendIPConfiguration{
Name: frontendIPv6Name,
Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{
PrivateIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodDynamic),
PrivateIPAddressVersion: to.Ptr(armnetwork.IPVersionIPv6),
PublicIPAddress: pipv6,
},
})
}
Copy link

@coderabbitai coderabbitai bot Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add an IPv6 API rule for the new -v6 frontend.

Both dual-stack code paths add an IPv6 frontend, but they only create api-v4. The IPv6 public IP will exist without any 6443 forwarding rule, so the API stays IPv4-only.

💡 Suggested change
 if in.isDualstack {
 	loadBalancer.Properties.FrontendIPConfigurations = append(loadBalancer.Properties.FrontendIPConfigurations,
 		&armnetwork.FrontendIPConfiguration{
 			Name: frontendIPv6Name,
 			Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{
 				PrivateIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodDynamic),
 				PrivateIPAddressVersion:   to.Ptr(armnetwork.IPVersionIPv6),
 				PublicIPAddress:           pipv6,
 			},
 		})
+	loadBalancer.Properties.LoadBalancingRules = append(loadBalancer.Properties.LoadBalancingRules,
+		&armnetwork.LoadBalancingRule{
+			Name: to.Ptr("api-v6"),
+			Properties: &armnetwork.LoadBalancingRulePropertiesFormat{
+				Protocol:             to.Ptr(armnetwork.TransportProtocolTCP),
+				FrontendPort:         to.Ptr[int32](6443),
+				BackendPort:          to.Ptr[int32](6443),
+				IdleTimeoutInMinutes: to.Ptr[int32](30),
+				EnableFloatingIP:     to.Ptr(false),
+				LoadDistribution:     to.Ptr(armnetwork.LoadDistributionDefault),
+				FrontendIPConfiguration: &armnetwork.SubResource{
+					ID: to.Ptr(fmt.Sprintf("/%s/%s/frontendIPConfigurations/%s-v6", in.idPrefix, in.loadBalancerName, in.frontendIPConfigName)),
+				},
+				BackendAddressPool: &armnetwork.SubResource{
+					ID: to.Ptr(fmt.Sprintf("/%s/%s/backendAddressPools/%s", in.idPrefix, in.loadBalancerName, in.backendAddressPoolName)),
+				},
+				Probe: &armnetwork.SubResource{
+					ID: to.Ptr(fmt.Sprintf("/%s/%s/probes/%s", in.idPrefix, in.loadBalancerName, probeName)),
+				},
+			},
+		})
 }

Apply the same pattern in both createAPILoadBalancer and updateOutboundLoadBalancerToAPILoadBalancer.

Also applies to: 233-254

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/infrastructure/azure/network.go` around lines 144 - 179, The dual-stack
path adds an IPv6 frontend but never creates a matching IPv6 load-balancing
rule, so the API remains IPv4-only; update both createAPILoadBalancer and
updateOutboundLoadBalancerToAPILoadBalancer to add a second
armnetwork.LoadBalancingRule (e.g., name "api-v6") mirroring the "api-v4" rule
but referencing the IPv6 frontend (frontendIPv6Name), using the same
ports/probe/backend pool and appropriate IPv6 settings; ensure the new rule is
appended to LoadBalancingRules alongside the existing "api-v4" entry so the IPv6
public IP forwards 6443 traffic too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rules are in the backend pr i think

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rna-afk, understood — if the IPv6 load-balancing rules (e.g., api-v6 on port 6443) are being added in a separate backend PR, that makes sense as a staged approach. Just make sure that PR is merged before or alongside this one in production, since until those rules land the IPv6 frontend IP will exist but won't forward any API traffic. Would it help to open a follow-up issue to track the dependency?

Comment on lines +373 to +397
existingFrontEndIPConfig := intLB.Properties.FrontendIPConfigurations
if len(existingFrontEndIPConfig) == 0 {
return fmt.Errorf("could not get frontEndIPConfig for internal LB %s", *intLB.Name)
}

var subnetID string
if existingFrontEndIPConfig[0].Properties != nil && existingFrontEndIPConfig[0].Properties.Subnet != nil {
subnetID = *existingFrontEndIPConfig[0].Properties.Subnet.ID
}

existingFrontEndIPConfigName := *(existingFrontEndIPConfig[0].Name)
frontendIPv6Name := fmt.Sprintf("%s-v6", existingFrontEndIPConfigName)
frontendIPv6 := &armnetwork.FrontendIPConfiguration{
Name: to.Ptr(frontendIPv6Name),
Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{
PrivateIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodDynamic),
PrivateIPAddressVersion: to.Ptr(armnetwork.IPVersionIPv6),
},
}
if subnetID != "" {
frontendIPv6.Properties.Subnet = &armnetwork.Subnet{
ID: to.Ptr(subnetID),
}
}
intLB.Properties.FrontendIPConfigurations = append(intLB.Properties.FrontendIPConfigurations, frontendIPv6)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Make the internal -v6 frontend reconciliation idempotent.

This always appends {existing}-v6. pkg/asset/manifests/azure/cluster.go already declares the same frontend name for dual-stack, so once CAPZ reconciles the LB, PostProvision will submit duplicate frontend configs. A retry hits the same failure even without CAPZ.

💡 Suggested change
 	existingFrontEndIPConfigName := *(existingFrontEndIPConfig[0].Name)
 	frontendIPv6Name := fmt.Sprintf("%s-v6", existingFrontEndIPConfigName)
+	for _, cfg := range existingFrontEndIPConfig {
+		if cfg.Name != nil && *cfg.Name == frontendIPv6Name {
+			return nil
+		}
+	}
 	frontendIPv6 := &armnetwork.FrontendIPConfiguration{
 		Name: to.Ptr(frontendIPv6Name),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
existingFrontEndIPConfig := intLB.Properties.FrontendIPConfigurations
if len(existingFrontEndIPConfig) == 0 {
return fmt.Errorf("could not get frontEndIPConfig for internal LB %s", *intLB.Name)
}
var subnetID string
if existingFrontEndIPConfig[0].Properties != nil && existingFrontEndIPConfig[0].Properties.Subnet != nil {
subnetID = *existingFrontEndIPConfig[0].Properties.Subnet.ID
}
existingFrontEndIPConfigName := *(existingFrontEndIPConfig[0].Name)
frontendIPv6Name := fmt.Sprintf("%s-v6", existingFrontEndIPConfigName)
frontendIPv6 := &armnetwork.FrontendIPConfiguration{
Name: to.Ptr(frontendIPv6Name),
Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{
PrivateIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodDynamic),
PrivateIPAddressVersion: to.Ptr(armnetwork.IPVersionIPv6),
},
}
if subnetID != "" {
frontendIPv6.Properties.Subnet = &armnetwork.Subnet{
ID: to.Ptr(subnetID),
}
}
intLB.Properties.FrontendIPConfigurations = append(intLB.Properties.FrontendIPConfigurations, frontendIPv6)
existingFrontEndIPConfig := intLB.Properties.FrontendIPConfigurations
if len(existingFrontEndIPConfig) == 0 {
return fmt.Errorf("could not get frontEndIPConfig for internal LB %s", *intLB.Name)
}
var subnetID string
if existingFrontEndIPConfig[0].Properties != nil && existingFrontEndIPConfig[0].Properties.Subnet != nil {
subnetID = *existingFrontEndIPConfig[0].Properties.Subnet.ID
}
existingFrontEndIPConfigName := *(existingFrontEndIPConfig[0].Name)
frontendIPv6Name := fmt.Sprintf("%s-v6", existingFrontEndIPConfigName)
for _, cfg := range existingFrontEndIPConfig {
if cfg.Name != nil && *cfg.Name == frontendIPv6Name {
return nil
}
}
frontendIPv6 := &armnetwork.FrontendIPConfiguration{
Name: to.Ptr(frontendIPv6Name),
Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{
PrivateIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodDynamic),
PrivateIPAddressVersion: to.Ptr(armnetwork.IPVersionIPv6),
},
}
if subnetID != "" {
frontendIPv6.Properties.Subnet = &armnetwork.Subnet{
ID: to.Ptr(subnetID),
}
}
intLB.Properties.FrontendIPConfigurations = append(intLB.Properties.FrontendIPConfigurations, frontendIPv6)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/infrastructure/azure/network.go` around lines 373 - 397, The current
logic always appends a new IPv6 frontend config named frontendIPv6Name causing
duplicates; instead, scan existingFrontEndIPConfig (or
intLB.Properties.FrontendIPConfigurations) for an entry with Name ==
frontendIPv6Name and if found, update that entry's Properties (set
PrivateIPAllocationMethod, PrivateIPAddressVersion and Subnet.ID if subnetID !=
"") to ensure it has the desired IPv6 settings; only if no matching Name is
found, append the new frontendIPv6. Use the existing variables frontendIPv6Name,
frontendIPv6 (or create a properties struct), existingFrontEndIPConfig and
intLB.Properties.FrontendIPConfigurations to locate and mutate or append the
config so reconciliation is idempotent.

Comment on lines +533 to 557
// Determine if this is an IPv6 NAT rule by checking the frontend IP config name
isIPv6NatRule := strings.Contains(in.frontendIPConfigID, "-v6")

for i, ipConfig := range bootstrapInterface.Properties.IPConfigurations {
var ipVersion armnetwork.IPVersion
if ipConfig.Properties.PrivateIPAddressVersion != nil {
ipVersion = *ipConfig.Properties.PrivateIPAddressVersion
} else {
ipVersion = armnetwork.IPVersionIPv4
}

// Match NAT rule IP version to IP config version
isIPv6Config := ipVersion == armnetwork.IPVersionIPv6
if isIPv6NatRule != isIPv6Config {
continue
}

// Add NAT rule to the first matching IP config and break
// (A NAT rule can only be attached to one IP config)
ipConfig.Properties.LoadBalancerInboundNatRules = append(ipConfig.Properties.LoadBalancerInboundNatRules,
&inboundNatRule,
)
bootstrapInterface.Properties.IPConfigurations[i] = ipConfig
break
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail when no bootstrap NIC IP configuration matches the NAT rule family.

After the IPv4/IPv6 filtering was added, this loop can fall through and still return success. That leaves the NAT rule on the load balancer but unattached to the bootstrap NIC.

💡 Suggested change
 	// Determine if this is an IPv6 NAT rule by checking the frontend IP config name
 	isIPv6NatRule := strings.Contains(in.frontendIPConfigID, "-v6")
+	matched := false
 
 	for i, ipConfig := range bootstrapInterface.Properties.IPConfigurations {
 		var ipVersion armnetwork.IPVersion
 		if ipConfig.Properties.PrivateIPAddressVersion != nil {
@@
 		ipConfig.Properties.LoadBalancerInboundNatRules = append(ipConfig.Properties.LoadBalancerInboundNatRules,
 			&inboundNatRule,
 		)
 		bootstrapInterface.Properties.IPConfigurations[i] = ipConfig
+		matched = true
 		break
 	}
+	if !matched {
+		return nil, fmt.Errorf("no bootstrap NIC IP configuration matches frontend %s", in.frontendIPConfigID)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/infrastructure/azure/network.go` around lines 533 - 557, The loop that
attaches inboundNatRule to bootstrapInterface.Properties.IPConfigurations can
fall through without attaching (due to the IPv4/IPv6 filter) but still return
success; add a boolean flag (e.g., attached) initialized false before the loop,
set it true when you append inboundNatRule to an IP config, and after the loop
check the flag and return an error if attached is false so the caller knows the
NAT rule was not attached to the bootstrap NIC (use the existing identifiers
bootstrapInterface, inboundNatRule, and in.frontendIPConfigID to locate the
logic).

@openshift-ci
Copy link
Contributor

openshift-ci bot commented Mar 9, 2026

@rna-afk: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/artifacts-images 7b6dffa link true /test artifacts-images
ci/prow/e2e-aws-ovn-rhcos10-devpreview 7b6dffa link false /test e2e-aws-ovn-rhcos10-devpreview
ci/prow/e2e-azurestack 7b6dffa link false /test e2e-azurestack
ci/prow/e2e-azure-ovn-shared-vpc 7b6dffa link false /test e2e-azure-ovn-shared-vpc
ci/prow/azure-private 7b6dffa link false /test azure-private
ci/prow/azure-ovn-marketplace-images 7b6dffa link false /test azure-ovn-marketplace-images
ci/prow/e2e-aws-ovn 7b6dffa link true /test e2e-aws-ovn

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants