Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
236b285
Add DiffTracker core types, state management, and sync operations
georgeedward2000 Mar 19, 2026
37e919a
addressed comments
georgeedward2000 May 20, 2026
cfaaaa7
address review comments: scope helper to DiffTracker; clarify NRP typ…
georgeedward2000 May 20, 2026
03bbd36
refactor: standardize SyncStatus constants and improve test assertions
georgeedward2000 Jun 9, 2026
522c44a
addressed comments:
georgeedward2000 Jun 15, 2026
abfa9ce
refactor: replace string array with switch statements for Operation, …
georgeedward2000 Jun 16, 2026
2e5c96e
Refactor difftracker: Improve logging, enhance equality checks, and a…
georgeedward2000 Jun 18, 2026
bd70126
difftracker: drop unused public DeepEqual wrapper
georgeedward2000 Jun 19, 2026
a852c29
addressed comments + added general improvements
georgeedward2000 Jun 22, 2026
b4f4657
difftracker: revert conditional empty-address delete to unconditional
georgeedward2000 Jun 23, 2026
462c9fc
difftracker: fix egress-remove inbound loss and gone-node address leak
georgeedward2000 Jun 23, 2026
2f81e3a
difftracker: revert gone-node address enumeration
georgeedward2000 Jun 23, 2026
0d05062
difftracker: rename isServiceReady to isServiceReadyToSync
georgeedward2000 Jun 23, 2026
5ee8d53
CP2: ServiceGateway Azure operations layer
georgeedward2000 Jun 16, 2026
e603285
CP2: harden ServiceGateway Azure operations layer
georgeedward2000 Jun 19, 2026
54bdc2d
Enhance Azure operations: add LoadBalancerSKUNameService constant, im…
georgeedward2000 Jun 24, 2026
5fd7ab2
Add unit tests for inbound and outbound resource name generation and …
georgeedward2000 Jun 24, 2026
f5c527e
Ensure unknown AddressUpdateAction defaults to PartialUpdate and add …
georgeedward2000 Jun 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pkg/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@ const (
LoadBalancerSKUStandard = "standard"
// LoadBalancerSKUService is the load balancer service SKU
LoadBalancerSKUService = "service"
// LoadBalancerSKUNameService is the case-sensitive ARM SKU name for the
// ServiceGateway inbound load balancer; it must be sent as "Service".
LoadBalancerSKUNameService = "Service"

// ServiceAnnotationLoadBalancerInternal is the annotation used on the service
ServiceAnnotationLoadBalancerInternal = "service.beta.kubernetes.io/azure-load-balancer-internal"
Expand Down Expand Up @@ -367,6 +370,8 @@ const (
FrontendIPConfigIDTemplate = "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/frontendIPConfigurations/%s"
// BackendPoolIDTemplate is the template of the backend pool
BackendPoolIDTemplate = "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/backendAddressPools/%s"
// NatGatewayIDTemplate is the template of the nat gateway
NatGatewayIDTemplate = "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/natGateways/%s"
// LoadBalancerProbeIDTemplate is the template of the load balancer probe
LoadBalancerProbeIDTemplate = "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/probes/%s"

Expand Down
498 changes: 498 additions & 0 deletions pkg/provider/difftracker/azure_operations.go

Large diffs are not rendered by default.

390 changes: 390 additions & 0 deletions pkg/provider/difftracker/azure_operations_clients_test.go

Large diffs are not rendered by default.

128 changes: 128 additions & 0 deletions pkg/provider/difftracker/azure_operations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
Copyright 2026 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package difftracker

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)

func TestConvertServiceDTOsToServiceRequests_OutboundRemovalHasNoNatGatewayID(t *testing.T) {
reqs, err := convertServiceDTOsToServiceRequests([]ServiceDTO{
{Service: "egr1", ServiceType: Outbound, IsDelete: true},
}, Config{SubscriptionID: "sub", ResourceGroup: "rg", VNetName: "vnet"})
assert.NoError(t, err)
assert.Len(t, reqs, 1)
assert.Nil(t, reqs[0].Service.Properties.PublicNatGatewayID)
}

func TestConvertServiceDTOsToServiceRequests_OutboundAddHasNatGatewayID(t *testing.T) {
natID := "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/natGateways/egr1"
reqs, err := convertServiceDTOsToServiceRequests([]ServiceDTO{
{Service: "egr1", ServiceType: Outbound, PublicNatGateway: NatGatewayDTO{Id: natID}},
}, Config{SubscriptionID: "sub", ResourceGroup: "rg", VNetName: "vnet"})
assert.NoError(t, err)
assert.Len(t, reqs, 1)
if assert.NotNil(t, reqs[0].Service.Properties.PublicNatGatewayID) {
assert.Equal(t, natID, *reqs[0].Service.Properties.PublicNatGatewayID)
}
}

func TestConvertServiceDTOsToServiceRequests_UnknownServiceTypeErrors(t *testing.T) {
_, err := convertServiceDTOsToServiceRequests([]ServiceDTO{
{Service: "x", ServiceType: ServiceType("")},
}, Config{SubscriptionID: "sub", ResourceGroup: "rg", VNetName: "vnet"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "unknown ServiceType")
}

func TestExtractResourceChildName(t *testing.T) {
id := "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1/backendAddressPools/pool1"
assert.Equal(t, "pool1", extractResourceChildName(id, "backendAddressPools"))
assert.Equal(t, "", extractResourceChildName("/subscriptions/sub/loadBalancers/lb1", "backendAddressPools"))
assert.Equal(t, "", extractResourceChildName("", "backendAddressPools"))
}

func TestCreateOrUpdatePIPWithResponse_NilPip(t *testing.T) {
dt := &DiffTracker{}
_, err := dt.createOrUpdatePIPWithResponse(context.Background(), "rg", nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "pip is nil")
}

func TestUpdateServiceLoadBalancerStatus_PreservesDualStackAndHostname(t *testing.T) {
svc := &v1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "svc", Namespace: "ns", UID: "uid-1"},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{IP: "2001:db8::1"},
{Hostname: "example.com"},
},
},
},
}
kubeClient := fake.NewSimpleClientset(svc)
dt := &DiffTracker{kubeClient: kubeClient}

err := dt.updateServiceLoadBalancerStatus(context.Background(), "uid-1", "10.0.0.1")
assert.NoError(t, err)

got, err := kubeClient.CoreV1().Services("ns").Get(context.Background(), "svc", metav1.GetOptions{})
assert.NoError(t, err)
var ips, hosts []string
for _, ing := range got.Status.LoadBalancer.Ingress {
if ing.IP != "" {
ips = append(ips, ing.IP)
}
if ing.Hostname != "" {
hosts = append(hosts, ing.Hostname)
}
}
assert.Contains(t, ips, "10.0.0.1")
assert.Contains(t, ips, "2001:db8::1")
assert.Contains(t, hosts, "example.com")
}

func TestUpdateServiceLoadBalancerStatus_ReplacesStaleSameFamily(t *testing.T) {
svc := &v1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "svc", Namespace: "ns", UID: "uid-2"},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{{IP: "10.0.0.9"}},
},
},
}
kubeClient := fake.NewSimpleClientset(svc)
dt := &DiffTracker{kubeClient: kubeClient}

err := dt.updateServiceLoadBalancerStatus(context.Background(), "uid-2", "10.0.0.1")
assert.NoError(t, err)

got, err := kubeClient.CoreV1().Services("ns").Get(context.Background(), "svc", metav1.GetOptions{})
assert.NoError(t, err)
var ips []string
for _, ing := range got.Status.LoadBalancer.Ingress {
ips = append(ips, ing.IP)
}
assert.Equal(t, []string{"10.0.0.1"}, ips)
}
66 changes: 66 additions & 0 deletions pkg/provider/difftracker/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
Copyright 2026 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package difftracker

import "fmt"

// Config holds the configuration values needed by DiffTracker
// to perform Azure operations without depending on the entire AzureCloud struct
// This allows DiffTracker to be more modular and testable
type Config struct {
// Azure subscription ID
SubscriptionID string

// Azure resource group name
ResourceGroup string

// Azure location/region (e.g. "eastus2"). Distinct from the difftracker
// Location type, which identifies a node by IP.
Location string

// Service Gateway resource name
ServiceGatewayResourceName string

// Full Service Gateway resource ID
ServiceGatewayID string

// Virtual Network name (required for backend pool configuration)
VNetName string
}

// Validate checks if the configuration has all required fields
func (c *Config) Validate() error {
if c.SubscriptionID == "" {
return fmt.Errorf("config validation failed: SubscriptionID is required")
}
if c.ResourceGroup == "" {
return fmt.Errorf("config validation failed: ResourceGroup is required")
}
if c.Location == "" {
return fmt.Errorf("config validation failed: Location is required")
}
if c.ServiceGatewayResourceName == "" {
return fmt.Errorf("config validation failed: ServiceGatewayResourceName is required")
}
if c.ServiceGatewayID == "" {
return fmt.Errorf("config validation failed: ServiceGatewayID is required")
}
if c.VNetName == "" {
return fmt.Errorf("config validation failed: VNetName is required")
}
return nil
}
86 changes: 86 additions & 0 deletions pkg/provider/difftracker/difftracker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
Copyright 2026 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package difftracker

import (
"fmt"

"k8s.io/client-go/kubernetes"
"k8s.io/klog/v2"

"sigs.k8s.io/cloud-provider-azure/pkg/azclient"
)

// New creates and initializes a new DiffTracker with the given state and configuration.
// It validates the configuration and ensures all required dependencies are present.
// Returns an error if the configuration is invalid or if any required dependency is nil.
func New(k8s K8sState, nrp NRPState, config Config, networkClientFactory azclient.ClientFactory, kubeClient kubernetes.Interface) (*DiffTracker, error) {
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("difftracker.New: %w", err)
}

if networkClientFactory == nil {
return nil, fmt.Errorf("difftracker.New: networkClientFactory must not be nil")
}
if kubeClient == nil {
return nil, fmt.Errorf("difftracker.New: kubeClient must not be nil")
}

klog.V(2).Infof("difftracker.New: initializing with config: subscription=%s, resourceGroup=%s, location=%s, serviceGatewayResourceName=%s, serviceGatewayID=%s, vNetName=%s",
config.SubscriptionID, config.ResourceGroup, config.Location, config.ServiceGatewayResourceName, config.ServiceGatewayID, config.VNetName)

// The caller is expected to pass fully initialized state structs. A nil
// field is unexpected and indicates a programming error, so error out.
if k8s.Services == nil {
return nil, fmt.Errorf("difftracker.New: k8s.Services must not be nil")
}
if k8s.Egresses == nil {
return nil, fmt.Errorf("difftracker.New: k8s.Egresses must not be nil")
}
if k8s.Nodes == nil {
return nil, fmt.Errorf("difftracker.New: k8s.Nodes must not be nil")
}
if nrp.LoadBalancers == nil {
return nil, fmt.Errorf("difftracker.New: nrp.LoadBalancers must not be nil")
}
if nrp.NATGateways == nil {
return nil, fmt.Errorf("difftracker.New: nrp.NATGateways must not be nil")
}
if nrp.Locations == nil {
return nil, fmt.Errorf("difftracker.New: nrp.Locations must not be nil")
}

diffTracker := &DiffTracker{
K8sResources: k8s,
NRPResources: nrp,

// Configuration and clients
config: config,
networkClientFactory: networkClientFactory,
kubeClient: kubeClient,
}

// Seed the outbound ref-counter from egress pods already in the initial state
// so a later REMOVE can drive the counter to zero.
for _, node := range k8s.Nodes {
for _, pod := range node.Pods {
diffTracker.incrementOutboundRefCount(pod.PublicOutboundIdentity)
}
}

return diffTracker, nil
}
Loading
Loading