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