Skip to content

Commit c1dff73

Browse files
committed
[azure] Implement excess-ip-release support
Signed-off-by: jaredledvina <jared.ledvina@datadoghq.com>
1 parent 0e4992c commit c1dff73

13 files changed

Lines changed: 989 additions & 10 deletions

File tree

Documentation/cmdref/cilium-operator-azure.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Documentation/cmdref/cilium-operator.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

operator/cmd/provider_azure_flags.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,8 @@ func (hook *azureFlagsHooks) RegisterProviderFlag(cmd *cobra.Command, vp *viper.
3434
flags.Bool(operatorOption.AzureUsePrimaryAddress, false, "Use Azure IP address from interface's primary IPConfigurations")
3535
option.BindEnvWithLegacyEnvFallback(vp, operatorOption.AzureUsePrimaryAddress, "AZURE_USE_PRIMARY_ADDRESS")
3636

37+
flags.Bool(operatorOption.AzureReleaseExcessIPs, false, "Enable releasing excess free IP addresses from Azure network interfaces.")
38+
option.BindEnv(vp, operatorOption.AzureReleaseExcessIPs)
39+
3740
vp.BindPFlags(flags)
3841
}

operator/option/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ const (
148148
// primary IPConfiguration
149149
AzureUsePrimaryAddress = "azure-use-primary-address"
150150

151+
// AzureReleaseExcessIPs allows releasing excess free IP addresses from
152+
// Azure network interfaces. Enabling this option reduces waste of IP
153+
// addresses but may increase the number of API calls to Azure.
154+
AzureReleaseExcessIPs = "azure-release-excess-ips"
155+
151156
// LeaderElectionLeaseDuration is the duration that non-leader candidates will wait to
152157
// force acquire leadership
153158
LeaderElectionLeaseDuration = "leader-election-lease-duration"
@@ -349,6 +354,10 @@ type OperatorConfig struct {
349354
// primary IPConfiguration
350355
AzureUsePrimaryAddress bool
351356

357+
// AzureReleaseExcessIPs allows releasing excess free IP addresses from
358+
// Azure network interfaces.
359+
AzureReleaseExcessIPs bool
360+
352361
// AlibabaCloud options
353362

354363
// AlibabaCloudVPCID allow user to specific vpc
@@ -465,6 +474,7 @@ func (c *OperatorConfig) Populate(logger *slog.Logger, vp *viper.Viper) {
465474
c.AzureSubscriptionID = vp.GetString(AzureSubscriptionID)
466475
c.AzureResourceGroup = vp.GetString(AzureResourceGroup)
467476
c.AzureUsePrimaryAddress = vp.GetBool(AzureUsePrimaryAddress)
477+
c.AzureReleaseExcessIPs = vp.GetBool(AzureReleaseExcessIPs)
468478
c.AzureUserAssignedIdentityID = vp.GetString(AzureUserAssignedIdentityID)
469479

470480
// AlibabaCloud options

pkg/azure/api/api.go

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,10 @@ func parseInterface(iface *armnetwork.Interface, subnets ipamTypes.SubnetMap, us
342342
State: strings.ToLower(string(*ip.Properties.ProvisioningState)),
343343
}
344344

345+
if ip.Name != nil {
346+
addr.SetIPConfigName(*ip.Name)
347+
}
348+
345349
if ip.Properties.Subnet != nil {
346350
addr.Subnet = *ip.Properties.Subnet.ID
347351
if subnet, ok := subnets[addr.Subnet]; ok {
@@ -837,6 +841,216 @@ func (c *Client) AssignPrivateIpAddressesVM(ctx context.Context, subnetID, inter
837841
return nil
838842
}
839843

844+
// PrimaryReleaseError is returned by Unassign* when the requested release set
845+
// would drop one or more primary IPConfigurations from a NIC. Azure ARM
846+
// rejects such updates with an opaque NetworkingInternalOperationError, so
847+
// we detect the case pre-flight and refuse to issue the update.
848+
//
849+
// Returning this error keeps the IPAM framework from marking the IPs as
850+
// released in CiliumNode.Status.IPAM.ReleaseIPs, leaving the CRD in sync
851+
// with the live NIC state.
852+
type PrimaryReleaseError struct {
853+
// InterfaceName is the NIC the primary IPConfiguration belongs to.
854+
InterfaceName string
855+
// Items is either the IP addresses (VM path) or IPConfiguration names
856+
// (VMSS path) of the primaries that were requested for release.
857+
Items []string
858+
}
859+
860+
func (e *PrimaryReleaseError) Error() string {
861+
return fmt.Sprintf("interface %s: refusing to release primary IPConfiguration(s) %v", e.InterfaceName, e.Items)
862+
}
863+
864+
// dropMatchingIPConfigsVM partitions ipConfigs into those to keep and those
865+
// to drop based on releaseSet (set of IP addresses). Primary IPConfigurations
866+
// are always retained even if their IP appears in releaseSet; their IPs are
867+
// returned via primaryBlocked so the caller can refuse the update entirely.
868+
func dropMatchingIPConfigsVM(
869+
ipConfigs []*armnetwork.InterfaceIPConfiguration,
870+
releaseSet map[string]struct{},
871+
) (kept []*armnetwork.InterfaceIPConfiguration, dropped int, primaryBlocked []string) {
872+
kept = make([]*armnetwork.InterfaceIPConfiguration, 0, len(ipConfigs))
873+
for _, c := range ipConfigs {
874+
if c == nil || c.Properties == nil || c.Properties.PrivateIPAddress == nil {
875+
kept = append(kept, c)
876+
continue
877+
}
878+
ip := *c.Properties.PrivateIPAddress
879+
_, requested := releaseSet[ip]
880+
isPrimary := c.Properties.Primary != nil && *c.Properties.Primary
881+
switch {
882+
case requested && isPrimary:
883+
primaryBlocked = append(primaryBlocked, ip)
884+
kept = append(kept, c)
885+
case requested:
886+
dropped++
887+
default:
888+
kept = append(kept, c)
889+
}
890+
}
891+
return
892+
}
893+
894+
// dropMatchingIPConfigsVMSS partitions ipConfigs into those to keep and those
895+
// to drop based on releaseNames (set of IPConfiguration resource names). The
896+
// VMSS compute model only carries names and the Primary flag — IPs live in
897+
// the network model — so the caller passes names. Primary IPConfigurations
898+
// are always retained.
899+
func dropMatchingIPConfigsVMSS(
900+
ipConfigs []*armcompute.VirtualMachineScaleSetIPConfiguration,
901+
releaseNames map[string]struct{},
902+
) (kept []*armcompute.VirtualMachineScaleSetIPConfiguration, dropped int, primaryBlocked []string) {
903+
kept = make([]*armcompute.VirtualMachineScaleSetIPConfiguration, 0, len(ipConfigs))
904+
for _, c := range ipConfigs {
905+
if c == nil || c.Name == nil {
906+
kept = append(kept, c)
907+
continue
908+
}
909+
name := *c.Name
910+
_, requested := releaseNames[name]
911+
isPrimary := c.Properties != nil && c.Properties.Primary != nil && *c.Properties.Primary
912+
switch {
913+
case requested && isPrimary:
914+
primaryBlocked = append(primaryBlocked, name)
915+
kept = append(kept, c)
916+
case requested:
917+
dropped++
918+
default:
919+
kept = append(kept, c)
920+
}
921+
}
922+
return
923+
}
924+
925+
// UnassignPrivateIpAddressesVM unassigns the given private IP addresses from
926+
// the named NIC of a standalone VM.
927+
//
928+
// The Azure network model carries privateIPAddress on each IPConfiguration,
929+
// so matching by IP is straightforward. If any requested IP backs a primary
930+
// IPConfiguration the function returns *PrimaryReleaseError without issuing
931+
// the update.
932+
func (c *Client) UnassignPrivateIpAddressesVM(ctx context.Context, interfaceName string, addresses []string) error {
933+
if len(addresses) == 0 {
934+
return nil
935+
}
936+
937+
c.limiter.Limit(ctx, interfacesGet)
938+
sinceStart := spanstat.Start()
939+
940+
iface, err := c.interfaces.Get(ctx, c.resourceGroup, interfaceName, nil)
941+
c.metricsAPI.ObserveAPICall(interfacesGet, deriveStatus(err), sinceStart.Seconds())
942+
if err != nil {
943+
return fmt.Errorf("failed to get standalone instance's interface %s: %w", interfaceName, err)
944+
}
945+
946+
releaseSet := make(map[string]struct{}, len(addresses))
947+
for _, ip := range addresses {
948+
releaseSet[ip] = struct{}{}
949+
}
950+
951+
kept, dropped, primaryBlocked := dropMatchingIPConfigsVM(iface.Properties.IPConfigurations, releaseSet)
952+
if len(primaryBlocked) > 0 {
953+
return &PrimaryReleaseError{InterfaceName: interfaceName, Items: primaryBlocked}
954+
}
955+
if dropped == 0 {
956+
return nil
957+
}
958+
iface.Properties.IPConfigurations = kept
959+
960+
c.limiter.Limit(ctx, interfacesCreateOrUpdate)
961+
sinceStart = spanstat.Start()
962+
963+
poller, err := c.interfaces.BeginCreateOrUpdate(ctx, c.resourceGroup, interfaceName, iface.Interface, nil)
964+
defer func() {
965+
c.metricsAPI.ObserveAPICall(interfacesCreateOrUpdate, deriveStatus(err), sinceStart.Seconds())
966+
}()
967+
if err != nil {
968+
return fmt.Errorf("unable to update interface %s: %w", interfaceName, err)
969+
}
970+
if _, err := poller.PollUntilDone(ctx, nil); err != nil {
971+
return fmt.Errorf("error while waiting for interface CreateOrUpdate to complete for %s: %w", interfaceName, err)
972+
}
973+
974+
return nil
975+
}
976+
977+
// UnassignPrivateIpAddressesVMSS unassigns the IPConfigurations identified by
978+
// ipConfigNames from the named NIC of a VMSS instance.
979+
//
980+
// The Azure compute model exposes IPConfiguration name and Primary but not
981+
// privateIPAddress, so the caller must translate IPs to IPConfiguration names
982+
// using the in-memory mapping populated by parseInterface. If any requested
983+
// name backs a primary IPConfiguration the function returns
984+
// *PrimaryReleaseError without issuing the update.
985+
func (c *Client) UnassignPrivateIpAddressesVMSS(ctx context.Context, instanceID, vmssName, interfaceName string, ipConfigNames []string) error {
986+
if len(ipConfigNames) == 0 {
987+
return nil
988+
}
989+
990+
vmssGetOptions := &armcompute.VirtualMachineScaleSetVMsClientGetOptions{
991+
Expand: to.Ptr(armcompute.InstanceViewTypesInstanceView),
992+
}
993+
994+
c.limiter.Limit(ctx, virtualMachineScaleSetVMsGet)
995+
sinceStart := spanstat.Start()
996+
997+
result, err := c.virtualMachineScaleSetVMs.Get(ctx, c.resourceGroup, vmssName, instanceID, vmssGetOptions)
998+
c.metricsAPI.ObserveAPICall(virtualMachineScaleSetVMsGet, deriveStatus(err), sinceStart.Seconds())
999+
if err != nil {
1000+
return fmt.Errorf("failed to get VM %s from VMSS %s: %w", instanceID, vmssName, err)
1001+
}
1002+
1003+
var netIfConfig *armcompute.VirtualMachineScaleSetNetworkConfiguration
1004+
if result.Properties.NetworkProfileConfiguration != nil {
1005+
for _, nic := range result.Properties.NetworkProfileConfiguration.NetworkInterfaceConfigurations {
1006+
if nic.Name != nil && *nic.Name == interfaceName {
1007+
netIfConfig = nic
1008+
break
1009+
}
1010+
}
1011+
}
1012+
if netIfConfig == nil {
1013+
return fmt.Errorf("interface %s does not exist in VM %s", interfaceName, instanceID)
1014+
}
1015+
1016+
releaseNames := make(map[string]struct{}, len(ipConfigNames))
1017+
for _, name := range ipConfigNames {
1018+
releaseNames[name] = struct{}{}
1019+
}
1020+
1021+
kept, dropped, primaryBlocked := dropMatchingIPConfigsVMSS(netIfConfig.Properties.IPConfigurations, releaseNames)
1022+
if len(primaryBlocked) > 0 {
1023+
return &PrimaryReleaseError{InterfaceName: interfaceName, Items: primaryBlocked}
1024+
}
1025+
if dropped == 0 {
1026+
return nil
1027+
}
1028+
netIfConfig.Properties.IPConfigurations = kept
1029+
1030+
// Unset imageReference for the same reason as AssignPrivateIpAddressesVMSS:
1031+
// preserves a possibly Azure-Compute-Gallery image reference on update.
1032+
// See https://github.com/Azure/AKS/issues/1819.
1033+
if result.Properties.StorageProfile != nil {
1034+
result.Properties.StorageProfile.ImageReference = nil
1035+
}
1036+
1037+
c.limiter.Limit(ctx, virtualMachineScaleSetVMsUpdate)
1038+
sinceStart = spanstat.Start()
1039+
1040+
poller, err := c.virtualMachineScaleSetVMs.BeginUpdate(ctx, c.resourceGroup, vmssName, instanceID, result.VirtualMachineScaleSetVM, nil)
1041+
defer func() {
1042+
c.metricsAPI.ObserveAPICall(virtualMachineScaleSetVMsUpdate, deriveStatus(err), sinceStart.Seconds())
1043+
}()
1044+
if err != nil {
1045+
return fmt.Errorf("unable to update virtualMachineScaleSetVMs: %w", err)
1046+
}
1047+
if _, err := poller.PollUntilDone(ctx, nil); err != nil {
1048+
return fmt.Errorf("error while waiting for virtualMachineScaleSetVMs Update to complete: %w", err)
1049+
}
1050+
1051+
return nil
1052+
}
1053+
8401054
// AssignPublicIPAddressesVMSS assigns a public IP to a VMSS instance.
8411055
// The public IP is allocated from a Public IP Prefix matching publicIpTags
8421056
func (c *Client) AssignPublicIPAddressesVMSS(ctx context.Context, instanceID, vmssName string, publicIpTags ipamTypes.Tags) (string, error) {

0 commit comments

Comments
 (0)