diff --git a/api/v1alpha3/azuremachine_conversion.go b/api/v1alpha3/azuremachine_conversion.go index ae03ca080ba..601290a64fd 100644 --- a/api/v1alpha3/azuremachine_conversion.go +++ b/api/v1alpha3/azuremachine_conversion.go @@ -74,6 +74,11 @@ func (src *AzureMachine) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Diagnostics = restored.Spec.Diagnostics } + if restored.Spec.NetworkInterfaces != nil { + dst.Spec.NetworkInterfaces = restored.Spec.NetworkInterfaces + } + + //nolint:staticcheck // SubnetName is now deprecated, but the v1beta1 defaulting webhook will migrate it to the networkInterfaces field dst.Spec.SubnetName = restored.Spec.SubnetName dst.Status.LongRunningOperationStates = restored.Status.LongRunningOperationStates diff --git a/api/v1alpha3/azuremachinetemplate_conversion.go b/api/v1alpha3/azuremachinetemplate_conversion.go index faf6673b62b..32c9a8d9f8b 100644 --- a/api/v1alpha3/azuremachinetemplate_conversion.go +++ b/api/v1alpha3/azuremachinetemplate_conversion.go @@ -62,6 +62,7 @@ func (src *AzureMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Spec.Diagnostics = restored.Spec.Template.Spec.Diagnostics } + //nolint:staticcheck // SubnetName is now deprecated, but the v1beta1 defaulting webhook will migrate it to the networkInterfaces field dst.Spec.Template.Spec.SubnetName = restored.Spec.Template.Spec.SubnetName dst.Spec.Template.ObjectMeta = restored.Spec.Template.ObjectMeta @@ -77,6 +78,10 @@ func (src *AzureMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Spec.SpotVMOptions.EvictionPolicy = restored.Spec.Template.Spec.SpotVMOptions.EvictionPolicy } + if restored.Spec.Template.Spec.NetworkInterfaces != nil { + dst.Spec.Template.Spec.NetworkInterfaces = restored.Spec.Template.Spec.NetworkInterfaces + } + return nil } diff --git a/api/v1alpha3/zz_generated.conversion.go b/api/v1alpha3/zz_generated.conversion.go index 609600b5dc5..62ead6593c4 100644 --- a/api/v1alpha3/zz_generated.conversion.go +++ b/api/v1alpha3/zz_generated.conversion.go @@ -925,6 +925,7 @@ func autoConvert_v1beta1_AzureMachineSpec_To_v1alpha3_AzureMachineSpec(in *v1bet // WARNING: in.SubnetName requires manual conversion: does not exist in peer-type // WARNING: in.DNSServers requires manual conversion: does not exist in peer-type // WARNING: in.VMExtensions requires manual conversion: does not exist in peer-type + // WARNING: in.NetworkInterfaces requires manual conversion: does not exist in peer-type return nil } diff --git a/api/v1alpha4/azuremachine_conversion.go b/api/v1alpha4/azuremachine_conversion.go index 31ad3e83fa3..0d5aa517fb7 100644 --- a/api/v1alpha4/azuremachine_conversion.go +++ b/api/v1alpha4/azuremachine_conversion.go @@ -60,6 +60,10 @@ func (src *AzureMachine) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Diagnostics = restored.Spec.Diagnostics } + if restored.Spec.NetworkInterfaces != nil { + dst.Spec.NetworkInterfaces = restored.Spec.NetworkInterfaces + } + return nil } diff --git a/api/v1alpha4/azuremachinetemplate_conversion.go b/api/v1alpha4/azuremachinetemplate_conversion.go index 1f4c6ae99ee..a6ecc51dc24 100644 --- a/api/v1alpha4/azuremachinetemplate_conversion.go +++ b/api/v1alpha4/azuremachinetemplate_conversion.go @@ -62,6 +62,10 @@ func (src *AzureMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Spec.SpotVMOptions.EvictionPolicy = restored.Spec.Template.Spec.SpotVMOptions.EvictionPolicy } + if restored.Spec.Template.Spec.NetworkInterfaces != nil { + dst.Spec.Template.Spec.NetworkInterfaces = restored.Spec.Template.Spec.NetworkInterfaces + } + return nil } diff --git a/api/v1alpha4/zz_generated.conversion.go b/api/v1alpha4/zz_generated.conversion.go index 922a2ceb565..0a5d960f000 100644 --- a/api/v1alpha4/zz_generated.conversion.go +++ b/api/v1alpha4/zz_generated.conversion.go @@ -1072,6 +1072,7 @@ func autoConvert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec(in *v1bet out.SubnetName = in.SubnetName // WARNING: in.DNSServers requires manual conversion: does not exist in peer-type // WARNING: in.VMExtensions requires manual conversion: does not exist in peer-type + // WARNING: in.NetworkInterfaces requires manual conversion: does not exist in peer-type return nil } diff --git a/api/v1beta1/azuremachine_default.go b/api/v1beta1/azuremachine_default.go index 5ebd53a26e3..6a838d3972a 100644 --- a/api/v1beta1/azuremachine_default.go +++ b/api/v1beta1/azuremachine_default.go @@ -115,6 +115,34 @@ func (s *AzureMachineSpec) SetDiagnosticsDefaults() { } } +// SetNetworkInterfacesDefaults sets the defaults for the network interfaces. +func (s *AzureMachineSpec) SetNetworkInterfacesDefaults() { + // Ensure the deprecated fields and new fields are not populated simultaneously + if (s.SubnetName != "" || s.AcceleratedNetworking != nil) && len(s.NetworkInterfaces) > 0 { + // Both the deprecated and the new fields are both set, return without changes + // and reject the request in the validating webhook which runs later. + return + } + + if len(s.NetworkInterfaces) == 0 { + s.NetworkInterfaces = []NetworkInterface{ + { + SubnetName: s.SubnetName, + AcceleratedNetworking: s.AcceleratedNetworking, + }, + } + s.SubnetName = "" + s.AcceleratedNetworking = nil + } + + // Ensure that PrivateIPConfigs defaults to 1 if not specified. + for i := 0; i < len(s.NetworkInterfaces); i++ { + if s.NetworkInterfaces[i].PrivateIPConfigs == 0 { + s.NetworkInterfaces[i].PrivateIPConfigs = 1 + } + } +} + // SetDefaults sets to the defaults for the AzureMachineSpec. func (s *AzureMachineSpec) SetDefaults() { if err := s.SetDefaultSSHPublicKey(); err != nil { @@ -125,4 +153,5 @@ func (s *AzureMachineSpec) SetDefaults() { s.SetIdentityDefaults() s.SetSpotEvictionPolicyDefaults() s.SetDiagnosticsDefaults() + s.SetNetworkInterfacesDefaults() } diff --git a/api/v1beta1/azuremachine_default_test.go b/api/v1beta1/azuremachine_default_test.go index dce02b5a724..6d862140620 100644 --- a/api/v1beta1/azuremachine_default_test.go +++ b/api/v1beta1/azuremachine_default_test.go @@ -266,6 +266,87 @@ func TestAzureMachineSpec_SetDataDisksDefaults(t *testing.T) { } } +func TestAzureMachineSpec_SetNetworkInterfacesDefaults(t *testing.T) { + g := NewWithT(t) + + tests := []struct { + name string + machine *AzureMachine + want *AzureMachine + }{ + { + name: "defaulting webhook updates machine with deprecated subnetName field", + machine: &AzureMachine{ + Spec: AzureMachineSpec{ + SubnetName: "test-subnet", + }, + }, + want: &AzureMachine{ + Spec: AzureMachineSpec{ + SubnetName: "", + NetworkInterfaces: []NetworkInterface{ + { + SubnetName: "test-subnet", + PrivateIPConfigs: 1, + }, + }, + }, + }, + }, + { + name: "defaulting webhook updates machine with deprecated acceleratedNetworking field", + machine: &AzureMachine{ + Spec: AzureMachineSpec{ + SubnetName: "test-subnet", + AcceleratedNetworking: to.BoolPtr(true), + }, + }, + want: &AzureMachine{ + Spec: AzureMachineSpec{ + SubnetName: "", + AcceleratedNetworking: nil, + NetworkInterfaces: []NetworkInterface{ + { + SubnetName: "test-subnet", + PrivateIPConfigs: 1, + AcceleratedNetworking: to.BoolPtr(true), + }, + }, + }, + }, + }, + { + name: "defaulting webhook does nothing if both new and deprecated subnetName fields are set", + machine: &AzureMachine{ + Spec: AzureMachineSpec{ + SubnetName: "test-subnet", + NetworkInterfaces: []NetworkInterface{{ + SubnetName: "test-subnet", + }}, + }, + }, + want: &AzureMachine{ + Spec: AzureMachineSpec{ + SubnetName: "test-subnet", + AcceleratedNetworking: nil, + NetworkInterfaces: []NetworkInterface{ + { + SubnetName: "test-subnet", + }, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tc.machine.Spec.SetNetworkInterfacesDefaults() + g.Expect(tc.machine).To(Equal(tc.want)) + }) + } +} + func createMachineWithSSHPublicKey(sshPublicKey string) *AzureMachine { machine := hardcodedAzureMachineWithSSHKey(sshPublicKey) return machine diff --git a/api/v1beta1/azuremachine_types.go b/api/v1beta1/azuremachine_types.go index 08164734867..0ec8f78238b 100644 --- a/api/v1beta1/azuremachine_types.go +++ b/api/v1beta1/azuremachine_types.go @@ -100,9 +100,7 @@ type AzureMachineSpec struct { // +optional EnableIPForwarding bool `json:"enableIPForwarding,omitempty"` - // AcceleratedNetworking enables or disables Azure accelerated networking. If omitted, it will be set based on - // whether the requested VMSize supports accelerated networking. - // If AcceleratedNetworking is set to true with a VMSize that does not support it, Azure will return an error. + // Deprecated: AcceleratedNetworking should be set in the networkInterfaces field. // +kubebuilder:validation:nullable // +optional AcceleratedNetworking *bool `json:"acceleratedNetworking,omitempty"` @@ -120,7 +118,7 @@ type AzureMachineSpec struct { // +optional SecurityProfile *SecurityProfile `json:"securityProfile,omitempty"` - // SubnetName selects the Subnet where the VM will be placed + // Deprecated: SubnetName should be set in the networkInterfaces field. // +optional SubnetName string `json:"subnetName,omitempty"` @@ -131,6 +129,13 @@ type AzureMachineSpec struct { // VMExtensions specifies a list of extensions to be added to the virtual machine. // +optional VMExtensions []VMExtension `json:"vmExtensions,omitempty"` + + // NetworkInterfaces specifies a list of network interface configurations. + // If left unspecified, the VM will get a single network interface with a + // single IPConfig in the subnet specified in the cluster's node subnet field. + // The primary interface will be the first networkInterface specified (index 0) in the list. + // +optional + NetworkInterfaces []NetworkInterface `json:"networkInterfaces,omitempty"` } // SpotVMOptions defines the options relevant to running the Machine on Spot VMs. diff --git a/api/v1beta1/azuremachine_validation.go b/api/v1beta1/azuremachine_validation.go index 339f3b152f2..894664c858f 100644 --- a/api/v1beta1/azuremachine_validation.go +++ b/api/v1beta1/azuremachine_validation.go @@ -54,9 +54,32 @@ func ValidateAzureMachineSpec(spec AzureMachineSpec) field.ErrorList { allErrs = append(allErrs, errs...) } + if errs := ValidateNetwork(spec.SubnetName, spec.AcceleratedNetworking, spec.NetworkInterfaces, field.NewPath("networkInterfaces")); len(errs) > 0 { + allErrs = append(allErrs, errs...) + } + return allErrs } +// ValidateNetwork validates the network configuration. +func ValidateNetwork(subnetName string, acceleratedNetworking *bool, networkInterfaces []NetworkInterface, fldPath *field.Path) field.ErrorList { + if (networkInterfaces != nil) && len(networkInterfaces) > 0 && subnetName != "" { + return field.ErrorList{field.Invalid(fldPath, networkInterfaces, "cannot set both networkInterfaces and machine subnetName")} + } + + if (networkInterfaces != nil) && len(networkInterfaces) > 0 && acceleratedNetworking != nil { + return field.ErrorList{field.Invalid(fldPath, networkInterfaces, "cannot set both networkInterfaces and machine acceleratedNetworking")} + } + + for _, nic := range networkInterfaces { + if nic.PrivateIPConfigs < 1 { + return field.ErrorList{field.Invalid(fldPath, networkInterfaces, "number of privateIPConfigs per interface must be at least 1")} + } + } + + return field.ErrorList{} +} + // ValidateSSHKey validates an SSHKey. func ValidateSSHKey(sshKey string, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} diff --git a/api/v1beta1/azuremachine_validation_test.go b/api/v1beta1/azuremachine_validation_test.go index 9a743814e53..931bd668a34 100644 --- a/api/v1beta1/azuremachine_validation_test.go +++ b/api/v1beta1/azuremachine_validation_test.go @@ -694,3 +694,96 @@ func TestAzureMachine_ValidateDataDisksUpdate(t *testing.T) { }) } } + +func TestAzureMachine_ValidateNetwork(t *testing.T) { + g := NewWithT(t) + + tests := []struct { + name string + subnetName string + acceleratedNetworking *bool + networkInterfaces []NetworkInterface + wantErr bool + }{ + { + name: "valid config with deprecated network fields", + subnetName: "subnet1", + acceleratedNetworking: to.BoolPtr(true), + networkInterfaces: nil, + wantErr: false, + }, + { + name: "valid config with networkInterfaces fields", + subnetName: "", + acceleratedNetworking: nil, + networkInterfaces: []NetworkInterface{{ + SubnetName: "subnet1", + AcceleratedNetworking: to.BoolPtr(true), + PrivateIPConfigs: 1, + }}, + wantErr: false, + }, + { + name: "valid config with multiple networkInterfaces", + subnetName: "", + acceleratedNetworking: nil, + networkInterfaces: []NetworkInterface{ + { + SubnetName: "subnet1", + AcceleratedNetworking: to.BoolPtr(true), + PrivateIPConfigs: 1, + }, + { + SubnetName: "subnet2", + AcceleratedNetworking: to.BoolPtr(true), + PrivateIPConfigs: 30, + }, + }, + wantErr: false, + }, + { + name: "invalid config using both deprecated subnetName and networkInterfaces", + subnetName: "subnet1", + acceleratedNetworking: nil, + networkInterfaces: []NetworkInterface{{ + SubnetName: "subnet1", + AcceleratedNetworking: nil, + PrivateIPConfigs: 1, + }}, + wantErr: true, + }, + { + name: "invalid config using both deprecated acceleratedNetworking and networkInterfaces", + subnetName: "", + acceleratedNetworking: to.BoolPtr(true), + networkInterfaces: []NetworkInterface{{ + SubnetName: "subnet1", + AcceleratedNetworking: to.BoolPtr(true), + PrivateIPConfigs: 1, + }}, + wantErr: true, + }, + { + name: "invalid config setting privateIPConfigs to less than 1", + subnetName: "", + acceleratedNetworking: nil, + networkInterfaces: []NetworkInterface{{ + SubnetName: "subnet1", + AcceleratedNetworking: to.BoolPtr(true), + PrivateIPConfigs: 0, + }}, + wantErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := ValidateNetwork(test.subnetName, test.acceleratedNetworking, test.networkInterfaces, field.NewPath("networkInterfaces")) + if test.wantErr { + g.Expect(err).ToNot(BeEmpty()) + } else { + g.Expect(err).To(BeEmpty()) + } + }) + } +} diff --git a/api/v1beta1/azuremachine_webhook.go b/api/v1beta1/azuremachine_webhook.go index ba2e1c8c525..8f7f5f1700d 100644 --- a/api/v1beta1/azuremachine_webhook.go +++ b/api/v1beta1/azuremachine_webhook.go @@ -17,6 +17,8 @@ limitations under the License. package v1beta1 import ( + "reflect" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" @@ -152,6 +154,24 @@ func (m *AzureMachine) ValidateUpdate(oldRaw runtime.Object) error { } } + if !reflect.DeepEqual(m.Spec.NetworkInterfaces, old.Spec.NetworkInterfaces) { + // The defaulting webhook may have migrated values from the old SubnetName field to the new NetworkInterfaces format. + old.Spec.SetNetworkInterfacesDefaults() + + // The reconciler will populate the SubnetName on the first interface if the user left it blank. + if old.Spec.NetworkInterfaces[0].SubnetName == "" && m.Spec.NetworkInterfaces[0].SubnetName != "" { + old.Spec.NetworkInterfaces[0].SubnetName = m.Spec.NetworkInterfaces[0].SubnetName + } + + // Enforce immutability for all other changes to NetworkInterfaces. + if !reflect.DeepEqual(m.Spec.NetworkInterfaces, old.Spec.NetworkInterfaces) { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "networkInterfaces"), + m.Spec.NetworkInterfaces, "field is immutable"), + ) + } + } + if len(allErrs) == 0 { return nil } diff --git a/api/v1beta1/azuremachine_webhook_test.go b/api/v1beta1/azuremachine_webhook_test.go index 03ce5906052..10c50986cec 100644 --- a/api/v1beta1/azuremachine_webhook_test.go +++ b/api/v1beta1/azuremachine_webhook_test.go @@ -128,6 +128,21 @@ func TestAzureMachine_ValidateCreate(t *testing.T) { machine: createMachineWithDiagnostics(UserManagedDiagnosticsStorage, nil), wantErr: true, }, + { + name: "azuremachine with invalid network configuration", + machine: createMachineWithNetworkConfig("subnet", nil, []NetworkInterface{{SubnetName: "subnet1"}}), + wantErr: true, + }, + { + name: "azuremachine with valid legacy network configuration", + machine: createMachineWithNetworkConfig("subnet", nil, []NetworkInterface{}), + wantErr: false, + }, + { + name: "azuremachine with valid network configuration", + machine: createMachineWithNetworkConfig("", nil, []NetworkInterface{{SubnetName: "subnet", PrivateIPConfigs: 1}}), + wantErr: false, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -604,6 +619,34 @@ func TestAzureMachine_ValidateUpdate(t *testing.T) { }, wantErr: true, }, + { + name: "validTest: azuremachine.spec.networkInterfaces is immutable", + oldMachine: &AzureMachine{ + Spec: AzureMachineSpec{ + NetworkInterfaces: []NetworkInterface{{SubnetName: "subnet"}}, + }, + }, + newMachine: &AzureMachine{ + Spec: AzureMachineSpec{ + NetworkInterfaces: []NetworkInterface{{SubnetName: "subnet"}}, + }, + }, + wantErr: false, + }, + { + name: "invalidtest: azuremachine.spec.networkInterfaces is immutable", + oldMachine: &AzureMachine{ + Spec: AzureMachineSpec{ + NetworkInterfaces: []NetworkInterface{{SubnetName: "subnet1"}}, + }, + }, + newMachine: &AzureMachine{ + Spec: AzureMachineSpec{ + NetworkInterfaces: []NetworkInterface{{SubnetName: "subnet2"}}, + }, + }, + wantErr: true, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -645,6 +688,18 @@ func TestAzureMachine_Default(t *testing.T) { } } +func createMachineWithNetworkConfig(subnetName string, acceleratedNetworking *bool, interfaces []NetworkInterface) *AzureMachine { + return &AzureMachine{ + Spec: AzureMachineSpec{ + SubnetName: subnetName, + NetworkInterfaces: interfaces, + AcceleratedNetworking: acceleratedNetworking, + OSDisk: validOSDisk, + SSHPublicKey: validSSHPublicKey, + }, + } +} + func createMachineWithSharedImage(subscriptionID, resourceGroup, name, gallery, version string) *AzureMachine { image := &Image{ SharedGallery: &AzureSharedGalleryImage{ diff --git a/api/v1beta1/azuremachinetemplate_webhook.go b/api/v1beta1/azuremachinetemplate_webhook.go index cbb38bcc837..1d7bd8242ff 100644 --- a/api/v1beta1/azuremachinetemplate_webhook.go +++ b/api/v1beta1/azuremachinetemplate_webhook.go @@ -64,6 +64,10 @@ func (r *AzureMachineTemplate) ValidateCreate(ctx context.Context, obj runtime.O ) } + if (r.Spec.Template.Spec.NetworkInterfaces != nil) && len(r.Spec.Template.Spec.NetworkInterfaces) > 0 && r.Spec.Template.Spec.SubnetName != "" { + allErrs = append(allErrs, field.Invalid(field.NewPath("AzureMachineTemplate", "spec", "template", "spec", "networkInterfaces"), r.Spec.Template.Spec.NetworkInterfaces, "cannot set both NetworkInterfaces and machine SubnetName")) + } + if len(allErrs) == 0 { return nil } @@ -128,5 +132,6 @@ func (r *AzureMachineTemplate) Default(ctx context.Context, obj runtime.Object) } t.Spec.Template.Spec.SetDefaultCachingType() t.Spec.Template.Spec.SetDataDisksDefaults() + t.Spec.Template.Spec.SetNetworkInterfacesDefaults() return nil } diff --git a/api/v1beta1/azuremachinetemplate_webhook_test.go b/api/v1beta1/azuremachinetemplate_webhook_test.go index 7d229161b22..c1872d30658 100644 --- a/api/v1beta1/azuremachinetemplate_webhook_test.go +++ b/api/v1beta1/azuremachinetemplate_webhook_test.go @@ -278,6 +278,9 @@ func TestAzureMachineTemplate_ValidateUpdate(t *testing.T) { }, DataDisks: []DataDisk{}, SSHPublicKey: "fake ssh key", + NetworkInterfaces: []NetworkInterface{{ + PrivateIPConfigs: 1, + }}, }, }, }, @@ -331,6 +334,107 @@ func TestAzureMachineTemplate_ValidateUpdate(t *testing.T) { }, wantErr: true, }, + { + name: "AzureMachineTemplate with legacy subnetName updated to new networkInterfaces", + oldTemplate: &AzureMachineTemplate{ + Spec: AzureMachineTemplateSpec{ + Template: AzureMachineTemplateResource{ + Spec: AzureMachineSpec{ + VMSize: "size", + FailureDomain: &failureDomain, + OSDisk: OSDisk{ + OSType: "type", + DiskSizeGB: to.Int32Ptr(11), + CachingType: "None", + }, + DataDisks: []DataDisk{}, + SSHPublicKey: "fake ssh key", + SubnetName: "subnet1", + AcceleratedNetworking: to.BoolPtr(true), + }, + }, + }, + }, + template: &AzureMachineTemplate{ + Spec: AzureMachineTemplateSpec{ + Template: AzureMachineTemplateResource{ + Spec: AzureMachineSpec{ + VMSize: "size", + FailureDomain: &failureDomain, + OSDisk: OSDisk{ + OSType: "type", + DiskSizeGB: to.Int32Ptr(11), + CachingType: "None", + }, + DataDisks: []DataDisk{}, + SSHPublicKey: "fake ssh key", + SubnetName: "", + AcceleratedNetworking: nil, + NetworkInterfaces: []NetworkInterface{ + { + SubnetName: "subnet1", + AcceleratedNetworking: to.BoolPtr(true), + PrivateIPConfigs: 1, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "AzureMachineTemplate with modified networkInterfaces is immutable", + oldTemplate: &AzureMachineTemplate{ + Spec: AzureMachineTemplateSpec{ + Template: AzureMachineTemplateResource{ + Spec: AzureMachineSpec{ + VMSize: "size", + FailureDomain: &failureDomain, + OSDisk: OSDisk{ + OSType: "type", + DiskSizeGB: to.Int32Ptr(11), + CachingType: "None", + }, + DataDisks: []DataDisk{}, + SSHPublicKey: "fake ssh key", + NetworkInterfaces: []NetworkInterface{ + { + SubnetName: "subnet1", + AcceleratedNetworking: to.BoolPtr(true), + PrivateIPConfigs: 1, + }, + }, + }, + }, + }, + }, + template: &AzureMachineTemplate{ + Spec: AzureMachineTemplateSpec{ + Template: AzureMachineTemplateResource{ + Spec: AzureMachineSpec{ + VMSize: "size", + FailureDomain: &failureDomain, + OSDisk: OSDisk{ + OSType: "type", + DiskSizeGB: to.Int32Ptr(11), + CachingType: "None", + }, + DataDisks: []DataDisk{}, + SSHPublicKey: "fake ssh key", + NetworkInterfaces: []NetworkInterface{ + { + SubnetName: "subnet2", + AcceleratedNetworking: to.BoolPtr(true), + PrivateIPConfigs: 1, + }, + }, + }, + }, + }, + }, + wantErr: true, + }, } // dry-run=true diff --git a/api/v1beta1/types.go b/api/v1beta1/types.go index f9b66ebc4c6..abaf2e40dbd 100644 --- a/api/v1beta1/types.go +++ b/api/v1beta1/types.go @@ -635,6 +635,24 @@ type ServiceEndpointSpec struct { Locations []string `json:"locations"` } +// NetworkInterface defines a network interface. +type NetworkInterface struct { + // SubnetName specifies the subnet in which the new network interface will be placed. + SubnetName string `json:"subnetName,omitempty"` + + // PrivateIPConfigs specifies the number of private IP addresses to attach to the interface. + // Defaults to 1 if not specified. + // +optional + PrivateIPConfigs int `json:"privateIPConfigs,omitempty"` + + // AcceleratedNetworking enables or disables Azure accelerated networking. If omitted, it will be set based on + // whether the requested VMSize supports accelerated networking. + // If AcceleratedNetworking is set to true with a VMSize that does not support it, Azure will return an error. + // +kubebuilder:validation:nullable + // +optional + AcceleratedNetworking *bool `json:"acceleratedNetworking,omitempty"` +} + // GetControlPlaneSubnet returns the cluster control plane subnet. func (n *NetworkSpec) GetControlPlaneSubnet() (SubnetSpec, error) { for _, sn := range n.Subnets { diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 2d31ae363f9..f5731fd91a9 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -640,6 +640,13 @@ func (in *AzureMachineSpec) DeepCopyInto(out *AzureMachineSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.NetworkInterfaces != nil { + in, out := &in.NetworkInterfaces, &out.NetworkInterfaces + *out = make([]NetworkInterface, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureMachineSpec. @@ -1317,6 +1324,26 @@ func (in *NetworkClassSpec) DeepCopy() *NetworkClassSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkInterface) DeepCopyInto(out *NetworkInterface) { + *out = *in + if in.AcceleratedNetworking != nil { + in, out := &in.AcceleratedNetworking, &out.AcceleratedNetworking + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkInterface. +func (in *NetworkInterface) DeepCopy() *NetworkInterface { + if in == nil { + return nil + } + out := new(NetworkInterface) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NetworkSpec) DeepCopyInto(out *NetworkSpec) { *out = *in diff --git a/azure/defaults.go b/azure/defaults.go index d156aa6c6b4..1aae34270bf 100644 --- a/azure/defaults.go +++ b/azure/defaults.go @@ -174,7 +174,10 @@ func GenerateVNetLinkName(vnetName string) string { } // GenerateNICName generates the name of a network interface based on the name of a VM. -func GenerateNICName(machineName string) string { +func GenerateNICName(machineName string, multiNIC bool, index int) string { + if multiNIC { + return fmt.Sprintf("%s-nic-%d", machineName, index) + } return fmt.Sprintf("%s-nic", machineName) } diff --git a/azure/scope/clients.go b/azure/scope/clients.go index 0c2b424e51a..89075d5aafa 100644 --- a/azure/scope/clients.go +++ b/azure/scope/clients.go @@ -27,7 +27,6 @@ import ( "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/azure" "github.com/Azure/go-autorest/autorest/azure/auth" - azureutil "sigs.k8s.io/cluster-api-provider-azure/util/azure" ) diff --git a/azure/scope/machine.go b/azure/scope/machine.go index dd42518ee40..c2f839f69bb 100644 --- a/azure/scope/machine.go +++ b/azure/scope/machine.go @@ -229,50 +229,78 @@ func (m *MachineScope) InboundNatSpecs() []azure.ResourceSpecGetter { // NICSpecs returns the network interface specs. func (m *MachineScope) NICSpecs() []azure.ResourceSpecGetter { + nicSpecs := []azure.ResourceSpecGetter{} + + // For backwards compatibility we need to ensure the NIC Name does not change on existing machines + // created prior to multiple NIC support + isMultiNIC := len(m.AzureMachine.Spec.NetworkInterfaces) > 1 + + for i := 0; i < len(m.AzureMachine.Spec.NetworkInterfaces); i++ { + isPrimary := i == 0 + nicName := azure.GenerateNICName(m.Name(), isMultiNIC, i) + nicSpecs = append(nicSpecs, m.BuildNICSpec(nicName, m.AzureMachine.Spec.NetworkInterfaces[i], isPrimary)) + } + return nicSpecs +} + +// BuildNICSpec takes a NetworkInterface from the AzureMachineSpec and returns a NICSpec for use by the networkinterfaces service. +func (m *MachineScope) BuildNICSpec(nicName string, infrav1NetworkInterface infrav1.NetworkInterface, primaryNetworkInterface bool) *networkinterfaces.NICSpec { spec := &networkinterfaces.NICSpec{ - Name: azure.GenerateNICName(m.Name()), + Name: nicName, ResourceGroup: m.ResourceGroup(), Location: m.Location(), SubscriptionID: m.SubscriptionID(), MachineName: m.Name(), VNetName: m.Vnet().Name, VNetResourceGroup: m.Vnet().ResourceGroup, - SubnetName: m.AzureMachine.Spec.SubnetName, - AcceleratedNetworking: m.AzureMachine.Spec.AcceleratedNetworking, - DNSServers: m.AzureMachine.Spec.DNSServers, + AcceleratedNetworking: infrav1NetworkInterface.AcceleratedNetworking, IPv6Enabled: m.IsIPv6Enabled(), EnableIPForwarding: m.AzureMachine.Spec.EnableIPForwarding, + SubnetName: infrav1NetworkInterface.SubnetName, AdditionalTags: m.AdditionalTags(), ClusterName: m.ClusterName(), + IPConfigs: []networkinterfaces.IPConfig{}, } - if m.Role() == infrav1.ControlPlane { - spec.PublicLBName = m.OutboundLBName(m.Role()) - spec.PublicLBAddressPoolName = m.OutboundPoolName(m.OutboundLBName(m.Role())) - if m.IsAPIServerPrivate() { - spec.InternalLBName = m.APIServerLBName() - spec.InternalLBAddressPoolName = m.APIServerLBPoolName(m.APIServerLBName()) - } else { - spec.PublicLBNATRuleName = m.Name() - spec.PublicLBAddressPoolName = m.APIServerLBPoolName(m.APIServerLBName()) - } + if m.cache != nil { + spec.SKU = &m.cache.VMSKU } - // If NAT gateway is not enabled and node has no public IP, then the NIC needs to reference the LB to get outbound traffic. - if m.Role() == infrav1.Node && !m.Subnet().IsNatGatewayEnabled() && !m.AzureMachine.Spec.AllocatePublicIP { - spec.PublicLBName = m.OutboundLBName(m.Role()) - spec.PublicLBAddressPoolName = m.OutboundPoolName(m.OutboundLBName(m.Role())) + for i := 0; i < infrav1NetworkInterface.PrivateIPConfigs; i++ { + spec.IPConfigs = append(spec.IPConfigs, networkinterfaces.IPConfig{}) } - if m.Role() == infrav1.Node && m.AzureMachine.Spec.AllocatePublicIP { - spec.PublicIPName = azure.GenerateNodePublicIPName(m.Name()) - } + if primaryNetworkInterface { + spec.DNSServers = m.AzureMachine.Spec.DNSServers - if m.cache != nil { - spec.SKU = &m.cache.VMSKU + if m.Role() == infrav1.ControlPlane { + spec.PublicLBName = m.OutboundLBName(m.Role()) + spec.PublicLBAddressPoolName = m.OutboundPoolName(m.OutboundLBName(m.Role())) + if m.IsAPIServerPrivate() { + spec.InternalLBName = m.APIServerLBName() + spec.InternalLBAddressPoolName = m.APIServerLBPoolName(m.APIServerLBName()) + } else { + spec.PublicLBNATRuleName = m.Name() + spec.PublicLBAddressPoolName = m.APIServerLBPoolName(m.APIServerLBName()) + } + } + + if m.Role() == infrav1.Node && m.AzureMachine.Spec.AllocatePublicIP { + spec.PublicIPName = azure.GenerateNodePublicIPName(m.Name()) + } + // If the NAT gateway is not enabled and node has no public IP, then the NIC needs to reference the LB to get outbound traffic. + if m.Role() == infrav1.Node && !m.Subnet().IsNatGatewayEnabled() && !m.AzureMachine.Spec.AllocatePublicIP { + spec.PublicLBName = m.OutboundLBName(m.Role()) + spec.PublicLBAddressPoolName = m.OutboundPoolName(m.OutboundLBName(m.Role())) + } + // If the NAT gateway is not enabled and node has no public IP, then the NIC needs to reference the LB to get outbound traffic. + if m.Role() == infrav1.Node && !m.Subnet().IsNatGatewayEnabled() && !m.AzureMachine.Spec.AllocatePublicIP { + spec.PublicLBName = m.OutboundLBName(m.Role()) + spec.PublicLBAddressPoolName = m.OutboundPoolName(m.OutboundLBName(m.Role())) + } } - return []azure.ResourceSpecGetter{spec} + return spec } // NICIDs returns the NIC resource IDs. @@ -282,6 +310,7 @@ func (m *MachineScope) NICIDs() []string { for i, nic := range nicspecs { nicIDs[i] = azure.NetworkInterfaceID(m.SubscriptionID(), nic.ResourceGroupName(), nic.ResourceName()) } + return nicIDs } @@ -365,7 +394,7 @@ func (m *MachineScope) VMExtensionSpecs() []azure.ResourceSpecGetter { // Subnet returns the machine's subnet. func (m *MachineScope) Subnet() infrav1.SubnetSpec { for _, subnet := range m.Subnets() { - if subnet.Name == m.AzureMachine.Spec.SubnetName { + if subnet.Name == m.AzureMachine.Spec.NetworkInterfaces[0].SubnetName { return subnet } } @@ -681,7 +710,7 @@ func (m *MachineScope) GetVMImage(ctx context.Context) (*infrav1.Image, error) { // Note: this logic exists only for purposes of ensuring backwards compatibility for old clusters created without the `subnetName` field being // set, and should be removed in the future when this field is no longer optional. func (m *MachineScope) SetSubnetName() error { - if m.AzureMachine.Spec.SubnetName == "" { + if m.AzureMachine.Spec.NetworkInterfaces[0].SubnetName == "" { subnetName := "" subnets := m.Subnets() var subnetCount int @@ -695,7 +724,7 @@ func (m *MachineScope) SetSubnetName() error { return errors.New("a subnet name must be specified when no subnets are specified or more than 1 subnet of the same role exist") } - m.AzureMachine.Spec.SubnetName = subnetName + m.AzureMachine.Spec.NetworkInterfaces[0].SubnetName = subnetName } return nil diff --git a/azure/scope/machine_test.go b/azure/scope/machine_test.go index a6fa8a53c4e..c314de8ac26 100644 --- a/azure/scope/machine_test.go +++ b/azure/scope/machine_test.go @@ -852,7 +852,9 @@ func TestMachineScope_Subnet(t *testing.T) { Name: "machine-name", }, Spec: infrav1.AzureMachineSpec{ - SubnetName: "machine-name-subnet", + NetworkInterfaces: []infrav1.NetworkInterface{{ + SubnetName: "machine-name-subnet", + }}, }, }, ClusterScoper: &ClusterScope{ @@ -1678,7 +1680,10 @@ func TestMachineScope_NICSpecs(t *testing.T) { }, Spec: infrav1.AzureMachineSpec{ ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"), - SubnetName: "subnet1", + NetworkInterfaces: []infrav1.NetworkInterface{{ + SubnetName: "subnet1", + PrivateIPConfigs: 1, + }}, }, }, Machine: &clusterv1.Machine{ @@ -1698,6 +1703,7 @@ func TestMachineScope_NICSpecs(t *testing.T) { SubscriptionID: "123", MachineName: "machine-name", SubnetName: "subnet1", + IPConfigs: []networkinterfaces.IPConfig{{}}, VNetName: "vnet1", VNetResourceGroup: "rg1", PublicLBName: "outbound-lb", @@ -1778,7 +1784,10 @@ func TestMachineScope_NICSpecs(t *testing.T) { }, Spec: infrav1.AzureMachineSpec{ ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"), - SubnetName: "subnet1", + NetworkInterfaces: []infrav1.NetworkInterface{{ + SubnetName: "subnet1", + PrivateIPConfigs: 1, + }}, }, }, Machine: &clusterv1.Machine{ @@ -1803,6 +1812,7 @@ func TestMachineScope_NICSpecs(t *testing.T) { SubscriptionID: "123", MachineName: "machine-name", SubnetName: "subnet1", + IPConfigs: []networkinterfaces.IPConfig{{}}, VNetName: "vnet1", VNetResourceGroup: "rg1", PublicLBName: "outbound-lb", @@ -1890,7 +1900,10 @@ func TestMachineScope_NICSpecs(t *testing.T) { }, Spec: infrav1.AzureMachineSpec{ ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"), - SubnetName: "subnet1", + NetworkInterfaces: []infrav1.NetworkInterface{{ + SubnetName: "subnet1", + PrivateIPConfigs: 1, + }}, }, }, Machine: &clusterv1.Machine{ @@ -1910,6 +1923,7 @@ func TestMachineScope_NICSpecs(t *testing.T) { SubscriptionID: "123", MachineName: "machine-name", SubnetName: "subnet1", + IPConfigs: []networkinterfaces.IPConfig{{}}, VNetName: "vnet1", VNetResourceGroup: "rg1", PublicLBName: "", @@ -1989,8 +2003,11 @@ func TestMachineScope_NICSpecs(t *testing.T) { Name: "machine", }, Spec: infrav1.AzureMachineSpec{ - ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"), - SubnetName: "subnet1", + ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"), + NetworkInterfaces: []infrav1.NetworkInterface{{ + SubnetName: "subnet1", + PrivateIPConfigs: 1, + }}, AllocatePublicIP: true, }, }, @@ -2011,6 +2028,7 @@ func TestMachineScope_NICSpecs(t *testing.T) { SubscriptionID: "123", MachineName: "machine-name", SubnetName: "subnet1", + IPConfigs: []networkinterfaces.IPConfig{{}}, VNetName: "vnet1", VNetResourceGroup: "rg1", PublicLBName: "", @@ -2097,7 +2115,10 @@ func TestMachineScope_NICSpecs(t *testing.T) { }, Spec: infrav1.AzureMachineSpec{ ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"), - SubnetName: "subnet1", + NetworkInterfaces: []infrav1.NetworkInterface{{ + SubnetName: "subnet1", + PrivateIPConfigs: 1, + }}, }, }, Machine: &clusterv1.Machine{ @@ -2117,6 +2138,7 @@ func TestMachineScope_NICSpecs(t *testing.T) { SubscriptionID: "123", MachineName: "machine-name", SubnetName: "subnet1", + IPConfigs: []networkinterfaces.IPConfig{{}}, VNetName: "vnet1", VNetResourceGroup: "rg1", PublicLBName: "", @@ -2200,7 +2222,10 @@ func TestMachineScope_NICSpecs(t *testing.T) { }, Spec: infrav1.AzureMachineSpec{ ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"), - SubnetName: "subnet1", + NetworkInterfaces: []infrav1.NetworkInterface{{ + SubnetName: "subnet1", + PrivateIPConfigs: 1, + }}, }, }, Machine: &clusterv1.Machine{ @@ -2220,6 +2245,7 @@ func TestMachineScope_NICSpecs(t *testing.T) { SubscriptionID: "123", MachineName: "machine-name", SubnetName: "subnet1", + IPConfigs: []networkinterfaces.IPConfig{{}}, VNetName: "vnet1", VNetResourceGroup: "rg1", PublicLBName: "api-lb", @@ -2303,7 +2329,10 @@ func TestMachineScope_NICSpecs(t *testing.T) { }, Spec: infrav1.AzureMachineSpec{ ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"), - SubnetName: "subnet1", + NetworkInterfaces: []infrav1.NetworkInterface{{ + SubnetName: "subnet1", + PrivateIPConfigs: 1, + }}, DNSServers: []string{"123.123.123.123", "124.124.124.124"}, }, }, @@ -2324,6 +2353,7 @@ func TestMachineScope_NICSpecs(t *testing.T) { SubscriptionID: "123", MachineName: "machine-name", SubnetName: "subnet1", + IPConfigs: []networkinterfaces.IPConfig{{}}, VNetName: "vnet1", VNetResourceGroup: "rg1", PublicLBName: "api-lb", @@ -2344,11 +2374,395 @@ func TestMachineScope_NICSpecs(t *testing.T) { }, }, }, + { + name: "Node Machine with multiple Network Interfaces", + machineScope: MachineScope{ + ClusterScoper: &ClusterScope{ + AzureClients: AzureClients{ + EnvironmentSettings: auth.EnvironmentSettings{ + Values: map[string]string{ + auth.SubscriptionID: "123", + }, + }, + }, + Cluster: &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + Namespace: "default", + }, + }, + AzureCluster: &infrav1.AzureCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "cluster.x-k8s.io/v1beta1", + Kind: "Cluster", + Name: "cluster", + }, + }, + }, + Spec: infrav1.AzureClusterSpec{ + ResourceGroup: "my-rg", + AzureClusterClassSpec: infrav1.AzureClusterClassSpec{ + Location: "westus", + }, + NetworkSpec: infrav1.NetworkSpec{ + Vnet: infrav1.VnetSpec{ + Name: "vnet1", + ResourceGroup: "rg1", + }, + Subnets: []infrav1.SubnetSpec{ + { + SubnetClassSpec: infrav1.SubnetClassSpec{ + Role: infrav1.SubnetNode, + Name: "subnet1", + }, + }, + }, + APIServerLB: infrav1.LoadBalancerSpec{ + Name: "api-lb", + }, + NodeOutboundLB: &infrav1.LoadBalancerSpec{ + Name: "outbound-lb", + }, + }, + }, + }, + }, + AzureMachine: &infrav1.AzureMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "machine", + }, + Spec: infrav1.AzureMachineSpec{ + ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"), + NetworkInterfaces: []infrav1.NetworkInterface{ + { + SubnetName: "subnet1", + AcceleratedNetworking: pointer.Bool(true), + PrivateIPConfigs: 1, + }, + { + SubnetName: "subnet2", + AcceleratedNetworking: pointer.Bool(true), + PrivateIPConfigs: 2, + }, + }, + }, + }, + Machine: &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "machine", + Labels: map[string]string{}, + }, + }, + }, + want: []azure.ResourceSpecGetter{ + &networkinterfaces.NICSpec{ + Name: "machine-name-nic-0", + ResourceGroup: "my-rg", + Location: "westus", + SubscriptionID: "123", + MachineName: "machine-name", + SubnetName: "subnet1", + IPConfigs: []networkinterfaces.IPConfig{{}}, + VNetName: "vnet1", + VNetResourceGroup: "rg1", + PublicLBName: "outbound-lb", + PublicLBAddressPoolName: "outbound-lb-outboundBackendPool", + PublicLBNATRuleName: "", + InternalLBName: "", + InternalLBAddressPoolName: "", + PublicIPName: "", + AcceleratedNetworking: pointer.Bool(true), + IPv6Enabled: false, + EnableIPForwarding: false, + SKU: nil, + ClusterName: "cluster", + AdditionalTags: map[string]string{ + "kubernetes.io_cluster_cluster": "owned", + }, + }, + &networkinterfaces.NICSpec{ + Name: "machine-name-nic-1", + ResourceGroup: "my-rg", + Location: "westus", + SubscriptionID: "123", + MachineName: "machine-name", + SubnetName: "subnet2", + IPConfigs: []networkinterfaces.IPConfig{{}, {}}, + VNetName: "vnet1", + VNetResourceGroup: "rg1", + PublicLBName: "", + PublicLBAddressPoolName: "", + PublicLBNATRuleName: "", + InternalLBName: "", + InternalLBAddressPoolName: "", + PublicIPName: "", + AcceleratedNetworking: pointer.Bool(true), + IPv6Enabled: false, + EnableIPForwarding: false, + SKU: nil, + ClusterName: "cluster", + AdditionalTags: map[string]string{ + "kubernetes.io_cluster_cluster": "owned", + }, + }, + }, + }, + { + name: "Node Machine with multiple Network Interfaces and Public IP Allocation enabled", + machineScope: MachineScope{ + ClusterScoper: &ClusterScope{ + AzureClients: AzureClients{ + EnvironmentSettings: auth.EnvironmentSettings{ + Values: map[string]string{ + auth.SubscriptionID: "123", + }, + }, + }, + Cluster: &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + Namespace: "default", + }, + }, + AzureCluster: &infrav1.AzureCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "cluster.x-k8s.io/v1beta1", + Kind: "Cluster", + Name: "cluster", + }, + }, + }, + Spec: infrav1.AzureClusterSpec{ + ResourceGroup: "my-rg", + AzureClusterClassSpec: infrav1.AzureClusterClassSpec{ + Location: "westus", + }, + NetworkSpec: infrav1.NetworkSpec{ + Vnet: infrav1.VnetSpec{ + Name: "vnet1", + ResourceGroup: "rg1", + }, + Subnets: []infrav1.SubnetSpec{ + { + SubnetClassSpec: infrav1.SubnetClassSpec{ + Role: infrav1.SubnetNode, + Name: "subnet1", + }, + }, + }, + APIServerLB: infrav1.LoadBalancerSpec{ + Name: "api-lb", + }, + NodeOutboundLB: &infrav1.LoadBalancerSpec{ + Name: "outbound-lb", + }, + }, + }, + }, + }, + AzureMachine: &infrav1.AzureMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "machine", + }, + Spec: infrav1.AzureMachineSpec{ + ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"), + AllocatePublicIP: true, + NetworkInterfaces: []infrav1.NetworkInterface{ + { + SubnetName: "subnet1", + AcceleratedNetworking: pointer.Bool(true), + PrivateIPConfigs: 1, + }, + { + SubnetName: "subnet2", + AcceleratedNetworking: pointer.Bool(true), + PrivateIPConfigs: 2, + }, + }, + }, + }, + Machine: &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "machine", + Labels: map[string]string{}, + }, + }, + }, + want: []azure.ResourceSpecGetter{ + &networkinterfaces.NICSpec{ + Name: "machine-name-nic-0", + ResourceGroup: "my-rg", + Location: "westus", + SubscriptionID: "123", + MachineName: "machine-name", + SubnetName: "subnet1", + IPConfigs: []networkinterfaces.IPConfig{{}}, + VNetName: "vnet1", + VNetResourceGroup: "rg1", + PublicLBName: "", + PublicLBAddressPoolName: "", + PublicLBNATRuleName: "", + InternalLBName: "", + InternalLBAddressPoolName: "", + PublicIPName: "pip-machine-name", + AcceleratedNetworking: pointer.Bool(true), + IPv6Enabled: false, + EnableIPForwarding: false, + SKU: nil, + ClusterName: "cluster", + AdditionalTags: map[string]string{ + "kubernetes.io_cluster_cluster": "owned", + }, + }, + &networkinterfaces.NICSpec{ + Name: "machine-name-nic-1", + ResourceGroup: "my-rg", + Location: "westus", + SubscriptionID: "123", + MachineName: "machine-name", + SubnetName: "subnet2", + IPConfigs: []networkinterfaces.IPConfig{{}, {}}, + VNetName: "vnet1", + VNetResourceGroup: "rg1", + PublicLBName: "", + PublicLBAddressPoolName: "", + PublicLBNATRuleName: "", + InternalLBName: "", + InternalLBAddressPoolName: "", + PublicIPName: "", + AcceleratedNetworking: pointer.Bool(true), + IPv6Enabled: false, + EnableIPForwarding: false, + SKU: nil, + ClusterName: "cluster", + AdditionalTags: map[string]string{ + "kubernetes.io_cluster_cluster": "owned", + }, + }, + }, + }, + { + name: "Node Machine with multiple IPConfigs", + machineScope: MachineScope{ + ClusterScoper: &ClusterScope{ + AzureClients: AzureClients{ + EnvironmentSettings: auth.EnvironmentSettings{ + Values: map[string]string{ + auth.SubscriptionID: "123", + }, + }, + }, + Cluster: &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + Namespace: "default", + }, + }, + AzureCluster: &infrav1.AzureCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "cluster.x-k8s.io/v1beta1", + Kind: "Cluster", + Name: "cluster", + }, + }, + }, + Spec: infrav1.AzureClusterSpec{ + ResourceGroup: "my-rg", + AzureClusterClassSpec: infrav1.AzureClusterClassSpec{ + Location: "westus", + }, + NetworkSpec: infrav1.NetworkSpec{ + Vnet: infrav1.VnetSpec{ + Name: "vnet1", + ResourceGroup: "rg1", + }, + Subnets: []infrav1.SubnetSpec{ + { + SubnetClassSpec: infrav1.SubnetClassSpec{ + Role: infrav1.SubnetNode, + Name: "subnet1", + }, + }, + }, + APIServerLB: infrav1.LoadBalancerSpec{ + Name: "api-lb", + }, + NodeOutboundLB: &infrav1.LoadBalancerSpec{ + Name: "outbound-lb", + }, + }, + }, + }, + }, + AzureMachine: &infrav1.AzureMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "machine", + }, + Spec: infrav1.AzureMachineSpec{ + ProviderID: to.StringPtr("azure://compute/virtual-machines/machine-name"), + NetworkInterfaces: []infrav1.NetworkInterface{ + { + SubnetName: "subnet1", + AcceleratedNetworking: pointer.Bool(true), + PrivateIPConfigs: 10, + }, + }, + }, + }, + Machine: &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "machine", + Labels: map[string]string{}, + }, + }, + }, + want: []azure.ResourceSpecGetter{ + &networkinterfaces.NICSpec{ + Name: "machine-name-nic", + ResourceGroup: "my-rg", + Location: "westus", + SubscriptionID: "123", + MachineName: "machine-name", + SubnetName: "subnet1", + IPConfigs: []networkinterfaces.IPConfig{{}, {}, {}, {}, {}, {}, {}, {}, {}, {}}, + VNetName: "vnet1", + VNetResourceGroup: "rg1", + PublicLBName: "outbound-lb", + PublicLBAddressPoolName: "outbound-lb-outboundBackendPool", + PublicLBNATRuleName: "", + InternalLBName: "", + InternalLBAddressPoolName: "", + PublicIPName: "", + AcceleratedNetworking: pointer.Bool(true), + IPv6Enabled: false, + EnableIPForwarding: false, + SKU: nil, + ClusterName: "cluster", + AdditionalTags: map[string]string{ + "kubernetes.io_cluster_cluster": "owned", + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) gotNicSpecs := tt.machineScope.NICSpecs() if !reflect.DeepEqual(gotNicSpecs, tt.want) { + g.Expect(gotNicSpecs).To(BeEquivalentTo(tt.want)) t.Errorf("NICSpecs(), gotNicSpecs = %s, want %s", specArrayToString(gotNicSpecs), specArrayToString(tt.want)) } }) diff --git a/azure/scope/machinepool.go b/azure/scope/machinepool.go index f164d9bf3ff..5086bd689db 100644 --- a/azure/scope/machinepool.go +++ b/azure/scope/machinepool.go @@ -115,12 +115,12 @@ func (m *MachinePoolScope) ScaleSetSpec() azure.ScaleSetSpec { SSHKeyData: m.AzureMachinePool.Spec.Template.SSHPublicKey, OSDisk: m.AzureMachinePool.Spec.Template.OSDisk, DataDisks: m.AzureMachinePool.Spec.Template.DataDisks, - SubnetName: m.AzureMachinePool.Spec.Template.SubnetName, + SubnetName: m.AzureMachinePool.Spec.Template.NetworkInterfaces[0].SubnetName, VNetName: m.Vnet().Name, VNetResourceGroup: m.Vnet().ResourceGroup, PublicLBName: m.OutboundLBName(infrav1.Node), PublicLBAddressPoolName: azure.GenerateOutboundBackendAddressPoolName(m.OutboundLBName(infrav1.Node)), - AcceleratedNetworking: m.AzureMachinePool.Spec.Template.AcceleratedNetworking, + AcceleratedNetworking: m.AzureMachinePool.Spec.Template.NetworkInterfaces[0].AcceleratedNetworking, Identity: m.AzureMachinePool.Spec.Identity, UserAssignedIdentities: m.AzureMachinePool.Spec.UserAssignedIdentities, DiagnosticsProfile: m.AzureMachinePool.Spec.Template.Diagnostics, @@ -128,6 +128,7 @@ func (m *MachinePoolScope) ScaleSetSpec() azure.ScaleSetSpec { SpotVMOptions: m.AzureMachinePool.Spec.Template.SpotVMOptions, FailureDomains: m.MachinePool.Spec.FailureDomains, TerminateNotificationTimeout: m.AzureMachinePool.Spec.Template.TerminateNotificationTimeout, + NetworkInterfaces: m.AzureMachinePool.Spec.Template.NetworkInterfaces, } } @@ -655,7 +656,7 @@ func (m *MachinePoolScope) getDeploymentStrategy() machinepool.TypedDeleteSelect // Note: this logic exists only for purposes of ensuring backwards compatibility for old clusters created without the `subnetName` field being // set, and should be removed in the future when this field is no longer optional. func (m *MachinePoolScope) SetSubnetName() error { - if m.AzureMachinePool.Spec.Template.SubnetName == "" { + if m.AzureMachinePool.Spec.Template.NetworkInterfaces[0].SubnetName == "" { subnetName := "" for _, subnet := range m.NodeSubnets() { subnetName = subnet.Name @@ -664,7 +665,7 @@ func (m *MachinePoolScope) SetSubnetName() error { return errors.New("a subnet name must be specified when no subnets are specified or more than 1 subnet of role 'node' exist") } - m.AzureMachinePool.Spec.Template.SubnetName = subnetName + m.AzureMachinePool.Spec.Template.NetworkInterfaces[0].SubnetName = subnetName } return nil diff --git a/azure/scope/machinepool_test.go b/azure/scope/machinepool_test.go index fbc6ead0291..6939a08e9b7 100644 --- a/azure/scope/machinepool_test.go +++ b/azure/scope/machinepool_test.go @@ -97,6 +97,89 @@ func TestMachinePoolScope_Name(t *testing.T) { }) } } +func TestMachinePoolScope_NetworkInterfaces(t *testing.T) { + tests := []struct { + name string + machinePoolScope MachinePoolScope + want int + }{ + { + name: "zero network interfaces", + machinePoolScope: MachinePoolScope{ + MachinePool: nil, + AzureMachinePool: &infrav1exp.AzureMachinePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default-nics", + }, + Spec: infrav1exp.AzureMachinePoolSpec{ + Template: infrav1exp.AzureMachinePoolMachineTemplate{ + AcceleratedNetworking: to.BoolPtr(true), + SubnetName: "node-subnet", + }, + }, + }, + ClusterScoper: nil, + }, + want: 0, + }, + { + name: "one network interface", + machinePoolScope: MachinePoolScope{ + MachinePool: nil, + AzureMachinePool: &infrav1exp.AzureMachinePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "single-nic", + }, + Spec: infrav1exp.AzureMachinePoolSpec{ + Template: infrav1exp.AzureMachinePoolMachineTemplate{ + NetworkInterfaces: []infrav1.NetworkInterface{ + { + SubnetName: "node-subnet", + }, + }, + }, + }, + }, + ClusterScoper: nil, + }, + want: 1, + }, + { + name: "two network interfaces", + machinePoolScope: MachinePoolScope{ + MachinePool: nil, + AzureMachinePool: &infrav1exp.AzureMachinePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dual-nics", + }, + Spec: infrav1exp.AzureMachinePoolSpec{ + Template: infrav1exp.AzureMachinePoolMachineTemplate{ + NetworkInterfaces: []infrav1.NetworkInterface{ + { + SubnetName: "control-plane-subnet", + }, + { + SubnetName: "node-subnet", + }, + }, + }, + }, + }, + ClusterScoper: nil, + }, + want: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := len(tt.machinePoolScope.AzureMachinePool.Spec.Template.NetworkInterfaces) + if got != tt.want { + t.Errorf("MachinePoolScope.Name() = %v, want %v", got, tt.want) + } + }) + } +} func TestMachinePoolScope_SetBootstrapConditions(t *testing.T) { cases := []struct { @@ -309,7 +392,6 @@ func TestMachinePoolScope_GetVMImage(t *testing.T) { clusterMock.EXPECT().BaseURI().AnyTimes() clusterMock.EXPECT().Location().AnyTimes() clusterMock.EXPECT().SubscriptionID().AnyTimes() - cases := []struct { Name string Setup func(mp *expv1.MachinePool, amp *infrav1exp.AzureMachinePool) diff --git a/azure/services/networkinterfaces/networkinterfaces_test.go b/azure/services/networkinterfaces/networkinterfaces_test.go index 5d20a15ded3..59af9f25bf3 100644 --- a/azure/services/networkinterfaces/networkinterfaces_test.go +++ b/azure/services/networkinterfaces/networkinterfaces_test.go @@ -58,6 +58,18 @@ var ( AcceleratedNetworking: nil, SKU: &fakeSku, } + fakeNICSpec3 = NICSpec{ + Name: "nic-3", + ResourceGroup: "my-rg", + Location: "fake-location", + SubscriptionID: "123", + MachineName: "azure-test1", + VNetName: "my-vnet", + VNetResourceGroup: "my-rg", + AcceleratedNetworking: nil, + SKU: &fakeSku, + IPConfigs: []IPConfig{{}, {}}, + } internalError = autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: http.StatusInternalServerError}, "Internal Server Error") ) @@ -83,6 +95,15 @@ func TestReconcileNetworkInterface(t *testing.T) { s.UpdatePutStatus(infrav1.NetworkInterfaceReadyCondition, serviceName, nil) }, }, + { + name: "successfully create a network interface with multiple IPConfigs", + expectedError: "", + expect: func(s *mock_networkinterfaces.MockNICScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) { + s.NICSpecs().Return([]azure.ResourceSpecGetter{&fakeNICSpec3}) + r.CreateOrUpdateResource(gomockinternal.AContext(), &fakeNICSpec3, serviceName).Return(nil, nil) + s.UpdatePutStatus(infrav1.NetworkInterfaceReadyCondition, serviceName, nil) + }, + }, { name: "successfully create multiple network interfaces", expectedError: "", diff --git a/azure/services/networkinterfaces/spec.go b/azure/services/networkinterfaces/spec.go index aba03939497..4e73cf6101a 100644 --- a/azure/services/networkinterfaces/spec.go +++ b/azure/services/networkinterfaces/spec.go @@ -18,6 +18,7 @@ package networkinterfaces import ( "context" + "strconv" "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-08-01/network" "github.com/Azure/go-autorest/autorest/to" @@ -52,6 +53,14 @@ type NICSpec struct { DNSServers []string AdditionalTags infrav1.Tags ClusterName string + IPConfigs []IPConfig +} + +// IPConfig defines the specification for an IP address configuration. +type IPConfig struct { + PrivateIP string + PublicIP bool + PublicIPAddress string } // ResourceName returns the name of the network interface. @@ -79,17 +88,19 @@ func (s *NICSpec) Parameters(ctx context.Context, existing interface{}) (paramet return nil, nil } - nicConfig := &network.InterfaceIPConfigurationPropertiesFormat{} + primaryIPConfig := &network.InterfaceIPConfigurationPropertiesFormat{ + Primary: to.BoolPtr(true), + } subnet := &network.Subnet{ ID: to.StringPtr(azure.SubnetID(s.SubscriptionID, s.VNetResourceGroup, s.VNetName, s.SubnetName)), } - nicConfig.Subnet = subnet + primaryIPConfig.Subnet = subnet - nicConfig.PrivateIPAllocationMethod = network.IPAllocationMethodDynamic + primaryIPConfig.PrivateIPAllocationMethod = network.IPAllocationMethodDynamic if s.StaticIPAddress != "" { - nicConfig.PrivateIPAllocationMethod = network.IPAllocationMethodStatic - nicConfig.PrivateIPAddress = to.StringPtr(s.StaticIPAddress) + primaryIPConfig.PrivateIPAllocationMethod = network.IPAllocationMethodStatic + primaryIPConfig.PrivateIPAddress = to.StringPtr(s.StaticIPAddress) } backendAddressPools := []network.BackendAddressPool{} @@ -101,7 +112,7 @@ func (s *NICSpec) Parameters(ctx context.Context, existing interface{}) (paramet }) } if s.PublicLBNATRuleName != "" { - nicConfig.LoadBalancerInboundNatRules = &[]network.InboundNatRule{ + primaryIPConfig.LoadBalancerInboundNatRules = &[]network.InboundNatRule{ { ID: to.StringPtr(azure.NATRuleID(s.SubscriptionID, s.ResourceGroup, s.PublicLBName, s.PublicLBNATRuleName)), }, @@ -114,10 +125,10 @@ func (s *NICSpec) Parameters(ctx context.Context, existing interface{}) (paramet ID: to.StringPtr(azure.AddressPoolID(s.SubscriptionID, s.ResourceGroup, s.InternalLBName, s.InternalLBAddressPoolName)), }) } - nicConfig.LoadBalancerBackendAddressPools = &backendAddressPools + primaryIPConfig.LoadBalancerBackendAddressPools = &backendAddressPools if s.PublicIPName != "" { - nicConfig.PublicIPAddress = &network.PublicIPAddress{ + primaryIPConfig.PublicIPAddress = &network.PublicIPAddress{ ID: to.StringPtr(azure.PublicIPID(s.SubscriptionID, s.ResourceGroup, s.PublicIPName)), } } @@ -140,10 +151,43 @@ func (s *NICSpec) Parameters(ctx context.Context, existing interface{}) (paramet ipConfigurations := []network.InterfaceIPConfiguration{ { Name: to.StringPtr("pipConfig"), - InterfaceIPConfigurationPropertiesFormat: nicConfig, + InterfaceIPConfigurationPropertiesFormat: primaryIPConfig, }, } + // Build additional IPConfigs if more than 1 is specified + for i := 1; i < len(s.IPConfigs); i++ { + c := s.IPConfigs[i] + newIPConfigPropertiesFormat := &network.InterfaceIPConfigurationPropertiesFormat{} + newIPConfigPropertiesFormat.Subnet = subnet + config := network.InterfaceIPConfiguration{ + Name: to.StringPtr(s.Name + "-" + strconv.Itoa(i)), + InterfaceIPConfigurationPropertiesFormat: newIPConfigPropertiesFormat, + } + if c.PrivateIP == "" { + config.InterfaceIPConfigurationPropertiesFormat.PrivateIPAllocationMethod = network.IPAllocationMethodDynamic + } else { + config.InterfaceIPConfigurationPropertiesFormat.PrivateIPAllocationMethod = network.IPAllocationMethodStatic + config.InterfaceIPConfigurationPropertiesFormat.PrivateIPAddress = &c.PrivateIP + } + + if c.PublicIP && c.PublicIPAddress != "" { + config.InterfaceIPConfigurationPropertiesFormat.PublicIPAddress = &network.PublicIPAddress{ + PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{ + PublicIPAllocationMethod: network.IPAllocationMethodStatic, + IPAddress: &c.PublicIPAddress, + }, + } + } else if c.PublicIP { + config.InterfaceIPConfigurationPropertiesFormat.PublicIPAddress = &network.PublicIPAddress{ + PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{ + PublicIPAllocationMethod: network.IPAllocationMethodDynamic, + }, + } + } + config.InterfaceIPConfigurationPropertiesFormat.Primary = to.BoolPtr(false) + ipConfigurations = append(ipConfigurations, config) + } if s.IPv6Enabled { ipv6Config := network.InterfaceIPConfiguration{ Name: to.StringPtr("ipConfigv6"), diff --git a/azure/services/networkinterfaces/spec_test.go b/azure/services/networkinterfaces/spec_test.go index a9addeedff6..466aa1107cc 100644 --- a/azure/services/networkinterfaces/spec_test.go +++ b/azure/services/networkinterfaces/spec_test.go @@ -24,6 +24,7 @@ import ( "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-08-01/network" "github.com/Azure/go-autorest/autorest/to" . "github.com/onsi/gomega" + "github.com/onsi/gomega/format" "sigs.k8s.io/cluster-api-provider-azure/azure/services/resourceskus" ) @@ -180,6 +181,74 @@ var ( DNSServers: fakeCustomDNSServers, ClusterName: "my-cluster", } + fakeDefaultIPconfigNICSpec = NICSpec{ + Name: "my-net-interface", + ResourceGroup: "my-rg", + Location: "fake-location", + SubscriptionID: "123", + MachineName: "azure-test1", + SubnetName: "my-subnet", + VNetName: "my-vnet", + IPv6Enabled: false, + VNetResourceGroup: "my-rg", + PublicLBName: "my-public-lb", + AcceleratedNetworking: nil, + SKU: &fakeSku, + EnableIPForwarding: true, + IPConfigs: []IPConfig{}, + ClusterName: "my-cluster", + } + fakeOneIPconfigNICSpec = NICSpec{ + Name: "my-net-interface", + ResourceGroup: "my-rg", + Location: "fake-location", + SubscriptionID: "123", + MachineName: "azure-test1", + SubnetName: "my-subnet", + VNetName: "my-vnet", + IPv6Enabled: false, + VNetResourceGroup: "my-rg", + PublicLBName: "my-public-lb", + AcceleratedNetworking: nil, + SKU: &fakeSku, + EnableIPForwarding: true, + IPConfigs: []IPConfig{{}}, + ClusterName: "my-cluster", + } + fakeTwoIPconfigNICSpec = NICSpec{ + Name: "my-net-interface", + ResourceGroup: "my-rg", + Location: "fake-location", + SubscriptionID: "123", + MachineName: "azure-test1", + SubnetName: "my-subnet", + VNetName: "my-vnet", + IPv6Enabled: false, + VNetResourceGroup: "my-rg", + PublicLBName: "my-public-lb", + AcceleratedNetworking: nil, + SKU: &fakeSku, + EnableIPForwarding: true, + IPConfigs: []IPConfig{{}, {}}, + ClusterName: "my-cluster", + } + fakeTwoIPconfigWithPublicNICSpec = NICSpec{ + Name: "my-net-interface", + ResourceGroup: "my-rg", + Location: "fake-location", + SubscriptionID: "123", + MachineName: "azure-test1", + SubnetName: "my-subnet", + VNetName: "my-vnet", + IPv6Enabled: false, + VNetResourceGroup: "my-rg", + PublicIPName: "pip-azure-test1", + AcceleratedNetworking: nil, + SKU: &fakeSku, + EnableIPForwarding: true, + IPConfigs: []IPConfig{{}, {}}, + ClusterName: "my-cluster", + } ) func TestParameters(t *testing.T) { @@ -212,6 +281,7 @@ func TestParameters(t *testing.T) { }, Location: to.StringPtr("fake-location"), InterfacePropertiesFormat: &network.InterfacePropertiesFormat{ + Primary: nil, EnableAcceleratedNetworking: to.BoolPtr(true), EnableIPForwarding: to.BoolPtr(false), DNSSettings: &network.InterfaceDNSSettings{}, @@ -219,6 +289,7 @@ func TestParameters(t *testing.T) { { Name: to.StringPtr("pipConfig"), InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{ + Primary: to.BoolPtr(true), LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/loadBalancers/my-public-lb/backendAddressPools/cluster-name-outboundBackendPool")}}, PrivateIPAllocationMethod: network.IPAllocationMethodStatic, PrivateIPAddress: to.StringPtr("fake.static.ip"), @@ -247,10 +318,12 @@ func TestParameters(t *testing.T) { EnableAcceleratedNetworking: to.BoolPtr(true), EnableIPForwarding: to.BoolPtr(false), DNSSettings: &network.InterfaceDNSSettings{}, + Primary: nil, IPConfigurations: &[]network.InterfaceIPConfiguration{ { Name: to.StringPtr("pipConfig"), InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{ + Primary: to.BoolPtr(true), LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/loadBalancers/my-public-lb/backendAddressPools/cluster-name-outboundBackendPool")}}, PrivateIPAllocationMethod: network.IPAllocationMethodDynamic, Subnet: &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")}, @@ -278,10 +351,12 @@ func TestParameters(t *testing.T) { EnableAcceleratedNetworking: to.BoolPtr(true), EnableIPForwarding: to.BoolPtr(false), DNSSettings: &network.InterfaceDNSSettings{}, + Primary: nil, IPConfigurations: &[]network.InterfaceIPConfiguration{ { Name: to.StringPtr("pipConfig"), InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{ + Primary: to.BoolPtr(true), Subnet: &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")}, PrivateIPAllocationMethod: network.IPAllocationMethodDynamic, LoadBalancerInboundNatRules: &[]network.InboundNatRule{{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/loadBalancers/my-public-lb/inboundNatRules/azure-test1")}}, @@ -309,6 +384,7 @@ func TestParameters(t *testing.T) { }, Location: to.StringPtr("fake-location"), InterfacePropertiesFormat: &network.InterfacePropertiesFormat{ + Primary: nil, EnableAcceleratedNetworking: to.BoolPtr(true), EnableIPForwarding: to.BoolPtr(false), DNSSettings: &network.InterfaceDNSSettings{}, @@ -316,6 +392,7 @@ func TestParameters(t *testing.T) { { Name: to.StringPtr("pipConfig"), InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{ + Primary: to.BoolPtr(true), Subnet: &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")}, PrivateIPAllocationMethod: network.IPAllocationMethodDynamic, LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{}, @@ -340,6 +417,7 @@ func TestParameters(t *testing.T) { }, Location: to.StringPtr("fake-location"), InterfacePropertiesFormat: &network.InterfacePropertiesFormat{ + Primary: nil, EnableAcceleratedNetworking: to.BoolPtr(false), EnableIPForwarding: to.BoolPtr(false), DNSSettings: &network.InterfaceDNSSettings{}, @@ -347,6 +425,7 @@ func TestParameters(t *testing.T) { { Name: to.StringPtr("pipConfig"), InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{ + Primary: to.BoolPtr(true), Subnet: &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")}, PrivateIPAllocationMethod: network.IPAllocationMethodDynamic, LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{}, @@ -371,6 +450,7 @@ func TestParameters(t *testing.T) { }, Location: to.StringPtr("fake-location"), InterfacePropertiesFormat: &network.InterfacePropertiesFormat{ + Primary: nil, EnableAcceleratedNetworking: to.BoolPtr(true), EnableIPForwarding: to.BoolPtr(true), DNSSettings: &network.InterfaceDNSSettings{}, @@ -378,6 +458,7 @@ func TestParameters(t *testing.T) { { Name: to.StringPtr("pipConfig"), InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{ + Primary: to.BoolPtr(true), Subnet: &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")}, PrivateIPAllocationMethod: network.IPAllocationMethodDynamic, LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{}, @@ -397,6 +478,159 @@ func TestParameters(t *testing.T) { }, expectedError: "", }, + { + name: "get parameters for network interface default ipconfig", + spec: &fakeDefaultIPconfigNICSpec, + existing: nil, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(BeAssignableToTypeOf(network.Interface{})) + g.Expect(result.(network.Interface)).To(Equal(network.Interface{ + Tags: map[string]*string{ + "Name": to.StringPtr("my-net-interface"), + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + }, + Location: to.StringPtr("fake-location"), + InterfacePropertiesFormat: &network.InterfacePropertiesFormat{ + Primary: nil, + EnableAcceleratedNetworking: to.BoolPtr(true), + EnableIPForwarding: to.BoolPtr(true), + DNSSettings: &network.InterfaceDNSSettings{}, + IPConfigurations: &[]network.InterfaceIPConfiguration{ + { + Name: to.StringPtr("pipConfig"), + InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{ + Primary: to.BoolPtr(true), + Subnet: &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")}, + PrivateIPAllocationMethod: network.IPAllocationMethodDynamic, + LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{}, + }, + }, + }, + }, + })) + }, + expectedError: "", + }, + { + name: "get parameters for network interface with one ipconfig", + spec: &fakeOneIPconfigNICSpec, + existing: nil, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(BeAssignableToTypeOf(network.Interface{})) + g.Expect(result.(network.Interface)).To(Equal(network.Interface{ + Tags: map[string]*string{ + "Name": to.StringPtr("my-net-interface"), + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + }, + Location: to.StringPtr("fake-location"), + InterfacePropertiesFormat: &network.InterfacePropertiesFormat{ + Primary: nil, + EnableAcceleratedNetworking: to.BoolPtr(true), + EnableIPForwarding: to.BoolPtr(true), + DNSSettings: &network.InterfaceDNSSettings{}, + IPConfigurations: &[]network.InterfaceIPConfiguration{ + { + Name: to.StringPtr("pipConfig"), + InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{ + Primary: to.BoolPtr(true), + Subnet: &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")}, + PrivateIPAllocationMethod: network.IPAllocationMethodDynamic, + LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{}, + }, + }, + }, + }, + })) + }, + expectedError: "", + }, + { + name: "get parameters for network interface with two ipconfigs", + spec: &fakeTwoIPconfigNICSpec, + existing: nil, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(BeAssignableToTypeOf(network.Interface{})) + g.Expect(result.(network.Interface)).To(Equal(network.Interface{ + Tags: map[string]*string{ + "Name": to.StringPtr("my-net-interface"), + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + }, + Location: to.StringPtr("fake-location"), + InterfacePropertiesFormat: &network.InterfacePropertiesFormat{ + Primary: nil, + EnableAcceleratedNetworking: to.BoolPtr(true), + EnableIPForwarding: to.BoolPtr(true), + DNSSettings: &network.InterfaceDNSSettings{}, + IPConfigurations: &[]network.InterfaceIPConfiguration{ + { + Name: to.StringPtr("pipConfig"), + InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{ + Primary: to.BoolPtr(true), + Subnet: &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")}, + PrivateIPAllocationMethod: network.IPAllocationMethodDynamic, + LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{}, + }, + }, + { + Name: to.StringPtr("my-net-interface-1"), + InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{ + Primary: to.BoolPtr(false), + Subnet: &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")}, + PrivateIPAllocationMethod: network.IPAllocationMethodDynamic, + LoadBalancerBackendAddressPools: nil, + }, + }, + }, + }, + })) + }, + expectedError: "", + }, + { + name: "get parameters for network interface with two ipconfigs and a public ip", + spec: &fakeTwoIPconfigWithPublicNICSpec, + existing: nil, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(BeAssignableToTypeOf(network.Interface{})) + g.Expect(result.(network.Interface)).To(Equal(network.Interface{ + Tags: map[string]*string{ + "Name": to.StringPtr("my-net-interface"), + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + }, + Location: to.StringPtr("fake-location"), + InterfacePropertiesFormat: &network.InterfacePropertiesFormat{ + Primary: nil, + EnableAcceleratedNetworking: to.BoolPtr(true), + EnableIPForwarding: to.BoolPtr(true), + DNSSettings: &network.InterfaceDNSSettings{}, + IPConfigurations: &[]network.InterfaceIPConfiguration{ + { + Name: to.StringPtr("pipConfig"), + InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{ + Primary: to.BoolPtr(true), + Subnet: &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")}, + PrivateIPAllocationMethod: network.IPAllocationMethodDynamic, + PublicIPAddress: &network.PublicIPAddress{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/publicIPAddresses/pip-azure-test1"), + }, + LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{}, + }, + }, + { + Name: to.StringPtr("my-net-interface-1"), + InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{ + Primary: to.BoolPtr(false), + Subnet: &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")}, + PrivateIPAllocationMethod: network.IPAllocationMethodDynamic, + LoadBalancerBackendAddressPools: nil, + }, + }, + }, + }, + })) + }, + expectedError: "", + }, { name: "get parameters for control plane network interface with DNS servers", spec: &fakeControlPlaneCustomDNSSettingsNICSpec, @@ -420,6 +654,7 @@ func TestParameters(t *testing.T) { Name: to.StringPtr("pipConfig"), InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{ Subnet: &network.Subnet{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet")}, + Primary: to.BoolPtr(true), PrivateIPAllocationMethod: network.IPAllocationMethodDynamic, LoadBalancerInboundNatRules: &[]network.InboundNatRule{{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/loadBalancers/my-public-lb/inboundNatRules/azure-test1")}}, LoadBalancerBackendAddressPools: &[]network.BackendAddressPool{ @@ -434,6 +669,7 @@ func TestParameters(t *testing.T) { expectedError: "", }, } + format.MaxLength = 10000 for _, tc := range testcases { tc := tc t.Run(tc.name, func(t *testing.T) { diff --git a/azure/services/scalesets/scalesets.go b/azure/services/scalesets/scalesets.go index a5f8aaa8bb1..1b52552d938 100644 --- a/azure/services/scalesets/scalesets.go +++ b/azure/services/scalesets/scalesets.go @@ -20,6 +20,7 @@ import ( "context" "encoding/base64" "fmt" + "strconv" "time" "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2021-11-01/compute" @@ -517,6 +518,68 @@ func (s *Service) buildVMSSFromSpec(ctx context.Context, vmssSpec azure.ScaleSet }, } + // Use custom NIC definitions in VMSS if set + if len(vmssSpec.NetworkInterfaces) > 0 { + nicConfigs := []compute.VirtualMachineScaleSetNetworkConfiguration{} + for i, n := range vmssSpec.NetworkInterfaces { + nicConfig := compute.VirtualMachineScaleSetNetworkConfiguration{} + nicConfig.VirtualMachineScaleSetNetworkConfigurationProperties = &compute.VirtualMachineScaleSetNetworkConfigurationProperties{} + nicConfig.Name = to.StringPtr(vmssSpec.Name + "-" + strconv.Itoa(i)) + if to.Bool(n.AcceleratedNetworking) { + nicConfig.VirtualMachineScaleSetNetworkConfigurationProperties.EnableAcceleratedNetworking = to.BoolPtr(true) + } else { + nicConfig.VirtualMachineScaleSetNetworkConfigurationProperties.EnableAcceleratedNetworking = to.BoolPtr(false) + } + if n.PrivateIPConfigs == 0 { + nicConfig.VirtualMachineScaleSetNetworkConfigurationProperties.IPConfigurations = &[]compute.VirtualMachineScaleSetIPConfiguration{ + { + Name: to.StringPtr(vmssSpec.Name + "-" + strconv.Itoa(i)), + VirtualMachineScaleSetIPConfigurationProperties: &compute.VirtualMachineScaleSetIPConfigurationProperties{ + Subnet: &compute.APIEntityReference{ + ID: to.StringPtr(azure.SubnetID(s.Scope.SubscriptionID(), vmssSpec.VNetResourceGroup, vmssSpec.VNetName, n.SubnetName)), + }, + Primary: to.BoolPtr(true), + PrivateIPAddressVersion: compute.IPVersionIPv4, + LoadBalancerBackendAddressPools: &backendAddressPools, + }, + }, + } + } else { + ipconfigs := []compute.VirtualMachineScaleSetIPConfiguration{} + + // Create IPConfigs + for j := 0; j < n.PrivateIPConfigs; j++ { + ipconfig := compute.VirtualMachineScaleSetIPConfiguration{ + Name: to.StringPtr(fmt.Sprintf("private-ipConfig-%v", j)), + VirtualMachineScaleSetIPConfigurationProperties: &compute.VirtualMachineScaleSetIPConfigurationProperties{ + PrivateIPAddressVersion: compute.IPVersionIPv4, + Subnet: &compute.APIEntityReference{ + ID: to.StringPtr(azure.SubnetID(s.Scope.SubscriptionID(), vmssSpec.VNetResourceGroup, vmssSpec.VNetName, n.SubnetName)), + }, + }, + } + + ipconfig.Subnet = &compute.APIEntityReference{ + ID: to.StringPtr(azure.SubnetID(s.Scope.SubscriptionID(), vmssSpec.VNetResourceGroup, vmssSpec.VNetName, n.SubnetName)), + } + ipconfigs = append(ipconfigs, ipconfig) + } + if i == 0 { + ipconfigs[0].LoadBalancerBackendAddressPools = &backendAddressPools + } + // Always use the first IPConfig as the Primary + ipconfigs[0].Primary = to.BoolPtr(true) + nicConfig.VirtualMachineScaleSetNetworkConfigurationProperties.IPConfigurations = &ipconfigs + } + nicConfigs = append(nicConfigs, nicConfig) + } + nicConfigs[0].VirtualMachineScaleSetNetworkConfigurationProperties.Primary = to.BoolPtr(true) + vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.NetworkProfile.NetworkInterfaceConfigurations = &nicConfigs + } else { + // Set default interface configuration if no custom ones are specified + vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.NetworkProfile.NetworkInterfaceConfigurations = s.getVirtualMachineScaleSetDefaultNetworkConfiguration(vmssSpec) + } + // Assign Identity to VMSS if vmssSpec.Identity == infrav1.VMIdentitySystemAssigned { vmss.Identity = &compute.VirtualMachineScaleSetIdentity{ @@ -572,6 +635,39 @@ func (s *Service) buildVMSSFromSpec(ctx context.Context, vmssSpec azure.ScaleSet return vmss, nil } +func (s *Service) getVirtualMachineScaleSetDefaultNetworkConfiguration(vmssSpec azure.ScaleSetSpec) *[]compute.VirtualMachineScaleSetNetworkConfiguration { + var backendAddressPools []compute.SubResource + if vmssSpec.PublicLBName != "" { + if vmssSpec.PublicLBAddressPoolName != "" { + backendAddressPools = append(backendAddressPools, + compute.SubResource{ + ID: to.StringPtr(azure.AddressPoolID(s.Scope.SubscriptionID(), s.Scope.ResourceGroup(), vmssSpec.PublicLBName, vmssSpec.PublicLBAddressPoolName)), + }) + } + } + return &[]compute.VirtualMachineScaleSetNetworkConfiguration{{ + Name: to.StringPtr(vmssSpec.Name), + VirtualMachineScaleSetNetworkConfigurationProperties: &compute.VirtualMachineScaleSetNetworkConfigurationProperties{ + Primary: to.BoolPtr(true), + EnableIPForwarding: to.BoolPtr(true), + IPConfigurations: &[]compute.VirtualMachineScaleSetIPConfiguration{ + { + Name: to.StringPtr(vmssSpec.Name), + VirtualMachineScaleSetIPConfigurationProperties: &compute.VirtualMachineScaleSetIPConfigurationProperties{ + Subnet: &compute.APIEntityReference{ + ID: to.StringPtr(azure.SubnetID(s.Scope.SubscriptionID(), vmssSpec.VNetResourceGroup, vmssSpec.VNetName, vmssSpec.SubnetName)), + }, + Primary: to.BoolPtr(true), + PrivateIPAddressVersion: compute.IPVersionIPv4, + LoadBalancerBackendAddressPools: &backendAddressPools, + }, + }, + }, + EnableAcceleratedNetworking: vmssSpec.AcceleratedNetworking, + }, + }} +} + // getVirtualMachineScaleSet provides information about a Virtual Machine Scale Set and its instances. func (s *Service) getVirtualMachineScaleSet(ctx context.Context, vmssName string) (*azure.VMSS, error) { ctx, _, done := tele.StartSpanWithLogger(ctx, "scalesets.Service.getVirtualMachineScaleSet") diff --git a/azure/services/scalesets/scalesets_test.go b/azure/services/scalesets/scalesets_test.go index 6598f3314dc..5ebc14c480c 100644 --- a/azure/services/scalesets/scalesets_test.go +++ b/azure/services/scalesets/scalesets_test.go @@ -258,6 +258,76 @@ func TestReconcileVMSS(t *testing.T) { setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS("VM_SIZE_AN"), putFuture) }, }, + { + name: "should start creating vmss with custom networking when specified", + expectedError: "failed to get VMSS my-vmss after create or update: failed to get result from future: operation type PUT on Azure resource my-rg/my-vmss is not done", + expect: func(g *WithT, s *mock_scalesets.MockScaleSetScopeMockRecorder, m *mock_scalesets.MockClientMockRecorder) { + spec := newDefaultVMSSSpec() + spec.DataDisks = append(spec.DataDisks, infrav1.DataDisk{ + NameSuffix: "my_disk_with_ultra_disks", + DiskSizeGB: 128, + Lun: to.Int32Ptr(3), + ManagedDisk: &infrav1.ManagedDiskParameters{ + StorageAccountType: "UltraSSD_LRS", + }, + }) + spec.NetworkInterfaces = []infrav1.NetworkInterface{ + { + SubnetName: "my-subnet", + PrivateIPConfigs: 1, + AcceleratedNetworking: pointer.Bool(true), + }, + { + SubnetName: "subnet2", + PrivateIPConfigs: 2, + AcceleratedNetworking: pointer.Bool(true), + }, + } + s.ScaleSetSpec().Return(spec).AnyTimes() + setupDefaultVMSSStartCreatingExpectations(s, m) + vmss := newDefaultVMSS("VM_SIZE") + vmss.VirtualMachineScaleSetProperties.AdditionalCapabilities = &compute.AdditionalCapabilities{UltraSSDEnabled: pointer.Bool(true)} + netConfigs := vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.NetworkProfile.NetworkInterfaceConfigurations + (*netConfigs)[0].Name = to.StringPtr("my-vmss-0") + (*netConfigs)[0].EnableIPForwarding = nil + nic1IPConfigs := (*netConfigs)[0].IPConfigurations + (*nic1IPConfigs)[0].Name = to.StringPtr("private-ipConfig-0") + (*nic1IPConfigs)[0].PrivateIPAddressVersion = compute.IPVersionIPv4 + (*netConfigs)[0].EnableAcceleratedNetworking = to.BoolPtr(true) + (*netConfigs)[0].Primary = to.BoolPtr(true) + vmssIPConfigs := []compute.VirtualMachineScaleSetIPConfiguration{ + { + Name: to.StringPtr("private-ipConfig-0"), + VirtualMachineScaleSetIPConfigurationProperties: &compute.VirtualMachineScaleSetIPConfigurationProperties{ + Primary: to.BoolPtr(true), + PrivateIPAddressVersion: compute.IPVersionIPv4, + Subnet: &compute.APIEntityReference{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/subnet2"), + }, + }, + }, + { + Name: to.StringPtr("private-ipConfig-1"), + VirtualMachineScaleSetIPConfigurationProperties: &compute.VirtualMachineScaleSetIPConfigurationProperties{ + PrivateIPAddressVersion: compute.IPVersionIPv4, + Subnet: &compute.APIEntityReference{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/subnet2"), + }, + }, + }, + } + *netConfigs = append(*netConfigs, compute.VirtualMachineScaleSetNetworkConfiguration{ + Name: to.StringPtr("my-vmss-1"), + VirtualMachineScaleSetNetworkConfigurationProperties: &compute.VirtualMachineScaleSetNetworkConfigurationProperties{ + EnableAcceleratedNetworking: to.BoolPtr(true), + IPConfigurations: &vmssIPConfigs, + }, + }) + m.CreateOrUpdateAsync(gomockinternal.AContext(), defaultResourceGroup, defaultVMSSName, gomockinternal.DiffEq(vmss)). + Return(putFuture, nil) + setupCreatingSucceededExpectations(s, m, newDefaultExistingVMSS("VM_SIZE"), putFuture) + }, + }, { name: "should start creating a vmss with spot vm", expectedError: "failed to get VMSS my-vmss after create or update: failed to get result from future: operation type PUT on Azure resource my-rg/my-vmss is not done", diff --git a/azure/types.go b/azure/types.go index 8b6598e73a3..c1dc9b2f05b 100644 --- a/azure/types.go +++ b/azure/types.go @@ -66,6 +66,7 @@ type ScaleSetSpec struct { DiagnosticsProfile *infrav1.Diagnostics FailureDomains []string VMExtensions []infrav1.VMExtension + NetworkInterfaces []infrav1.NetworkInterface } // TagsSpec defines the specification for a set of tags. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepools.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepools.yaml index e9842965d0b..e97903748fb 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepools.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepools.yaml @@ -1468,11 +1468,8 @@ spec: virtual machine within the Machine Pool properties: acceleratedNetworking: - description: AcceleratedNetworking enables or disables Azure accelerated - networking. If omitted, it will be set based on whether the - requested VMSize supports accelerated networking. If AcceleratedNetworking - is set to true with a VMSize that does not support it, Azure - will return an error. + description: 'Deprecated: AcceleratedNetworking should be set + in the networkInterfaces field.' type: boolean dataDisks: description: DataDisks specifies the list of data disks to be @@ -1751,6 +1748,33 @@ spec: - version type: object type: object + networkInterfaces: + description: NetworkInterfaces specifies a list of network interface + configurations. If left unspecified, the VM will get a single + network interface with a single IPConfig in the subnet specified + in the cluster's node subnet field. The primary interface will + be the first networkInterface specified (index 0) in the list. + items: + description: NetworkInterface defines a network interface. + properties: + acceleratedNetworking: + description: AcceleratedNetworking enables or disables Azure + accelerated networking. If omitted, it will be set based + on whether the requested VMSize supports accelerated networking. + If AcceleratedNetworking is set to true with a VMSize + that does not support it, Azure will return an error. + type: boolean + privateIPConfigs: + description: PrivateIPConfigs specifies the number of private + IP addresses to attach to the interface. Defaults to 1 + if not specified. + type: integer + subnetName: + description: SubnetName specifies the subnet in which the + new network interface will be placed. + type: string + type: object + type: array osDisk: description: OSDisk contains the operating system disk information for a Virtual Machine @@ -1837,8 +1861,8 @@ spec: encoded to add to a Virtual Machine type: string subnetName: - description: SubnetName selects the Subnet where the VMSS will - be placed + description: 'Deprecated: SubnetName should be set in the networkInterfaces + field.' type: string terminateNotificationTimeout: description: TerminateNotificationTimeout enables or disables diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml index 7516cf104b8..214faa90679 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml @@ -1044,11 +1044,8 @@ spec: description: AzureMachineSpec defines the desired state of AzureMachine. properties: acceleratedNetworking: - description: AcceleratedNetworking enables or disables Azure accelerated - networking. If omitted, it will be set based on whether the requested - VMSize supports accelerated networking. If AcceleratedNetworking - is set to true with a VMSize that does not support it, Azure will - return an error. + description: 'Deprecated: AcceleratedNetworking should be set in the + networkInterfaces field.' type: boolean additionalCapabilities: description: AdditionalCapabilities specifies additional capabilities @@ -1374,6 +1371,33 @@ spec: - version type: object type: object + networkInterfaces: + description: NetworkInterfaces specifies a list of network interface + configurations. If left unspecified, the VM will get a single network + interface with a single IPConfig in the subnet specified in the + cluster's node subnet field. The primary interface will be the first + networkInterface specified (index 0) in the list. + items: + description: NetworkInterface defines a network interface. + properties: + acceleratedNetworking: + description: AcceleratedNetworking enables or disables Azure + accelerated networking. If omitted, it will be set based on + whether the requested VMSize supports accelerated networking. + If AcceleratedNetworking is set to true with a VMSize that + does not support it, Azure will return an error. + type: boolean + privateIPConfigs: + description: PrivateIPConfigs specifies the number of private + IP addresses to attach to the interface. Defaults to 1 if + not specified. + type: integer + subnetName: + description: SubnetName specifies the subnet in which the new + network interface will be placed. + type: string + type: object + type: array osDisk: description: OSDisk specifies the parameters for the operating system disk of the machine @@ -1467,7 +1491,8 @@ spec: sshPublicKey: type: string subnetName: - description: SubnetName selects the Subnet where the VM will be placed + description: 'Deprecated: SubnetName should be set in the networkInterfaces + field.' type: string userAssignedIdentities: description: UserAssignedIdentities is a list of standalone Azure diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinetemplates.yaml index 2f0df657bc8..afe99143e37 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinetemplates.yaml @@ -812,11 +812,8 @@ spec: of the machine. properties: acceleratedNetworking: - description: AcceleratedNetworking enables or disables Azure - accelerated networking. If omitted, it will be set based - on whether the requested VMSize supports accelerated networking. - If AcceleratedNetworking is set to true with a VMSize that - does not support it, Azure will return an error. + description: 'Deprecated: AcceleratedNetworking should be + set in the networkInterfaces field.' type: boolean additionalCapabilities: description: AdditionalCapabilities specifies additional capabilities @@ -1159,6 +1156,35 @@ spec: - version type: object type: object + networkInterfaces: + description: NetworkInterfaces specifies a list of network + interface configurations. If left unspecified, the VM will + get a single network interface with a single IPConfig in + the subnet specified in the cluster's node subnet field. + The primary interface will be the first networkInterface + specified (index 0) in the list. + items: + description: NetworkInterface defines a network interface. + properties: + acceleratedNetworking: + description: AcceleratedNetworking enables or disables + Azure accelerated networking. If omitted, it will + be set based on whether the requested VMSize supports + accelerated networking. If AcceleratedNetworking is + set to true with a VMSize that does not support it, + Azure will return an error. + type: boolean + privateIPConfigs: + description: PrivateIPConfigs specifies the number of + private IP addresses to attach to the interface. Defaults + to 1 if not specified. + type: integer + subnetName: + description: SubnetName specifies the subnet in which + the new network interface will be placed. + type: string + type: object + type: array osDisk: description: OSDisk specifies the parameters for the operating system disk of the machine @@ -1253,8 +1279,8 @@ spec: sshPublicKey: type: string subnetName: - description: SubnetName selects the Subnet where the VM will - be placed + description: 'Deprecated: SubnetName should be set in the + networkInterfaces field.' type: string userAssignedIdentities: description: UserAssignedIdentities is a list of standalone diff --git a/controllers/azuremachine_reconciler.go b/controllers/azuremachine_reconciler.go index e80accfad0d..5eb0ef60bb9 100644 --- a/controllers/azuremachine_reconciler.go +++ b/controllers/azuremachine_reconciler.go @@ -73,6 +73,9 @@ func (s *azureMachineService) Reconcile(ctx context.Context) error { ctx, _, done := tele.StartSpanWithLogger(ctx, "controllers.azureMachineService.Reconcile") defer done() + // Ensure that the deprecated networking field values have been migrated to the new NetworkInterfaces field. + s.scope.AzureMachine.Spec.SetNetworkInterfacesDefaults() + if err := s.scope.SetSubnetName(); err != nil { return errors.Wrap(err, "failed defaulting subnet name") } diff --git a/exp/api/v1alpha3/azuremachinepool_conversion.go b/exp/api/v1alpha3/azuremachinepool_conversion.go index 9120505d052..e3c5c4e1f3c 100644 --- a/exp/api/v1alpha3/azuremachinepool_conversion.go +++ b/exp/api/v1alpha3/azuremachinepool_conversion.go @@ -49,6 +49,7 @@ func (src *AzureMachinePool) ConvertTo(dstRaw conversion.Hub) error { } } + //nolint:staticcheck // SubnetName is now deprecated, but the v1beta1 defaulting webhook will migrate it to the networkInterfaces field dst.Spec.Template.SubnetName = restored.Spec.Template.SubnetName dst.Spec.Strategy.Type = restored.Spec.Strategy.Type @@ -78,6 +79,10 @@ func (src *AzureMachinePool) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Image.ComputeGallery = restored.Spec.Template.Image.ComputeGallery } + if restored.Spec.Template.NetworkInterfaces != nil { + dst.Spec.Template.NetworkInterfaces = restored.Spec.Template.NetworkInterfaces + } + if len(dst.Annotations) == 0 { dst.Annotations = nil } diff --git a/exp/api/v1alpha3/zz_generated.conversion.go b/exp/api/v1alpha3/zz_generated.conversion.go index 1df50cfeb61..4572aedbd54 100644 --- a/exp/api/v1alpha3/zz_generated.conversion.go +++ b/exp/api/v1alpha3/zz_generated.conversion.go @@ -487,6 +487,7 @@ func autoConvert_v1beta1_AzureMachinePoolMachineTemplate_To_v1alpha3_AzureMachin } // WARNING: in.SubnetName requires manual conversion: does not exist in peer-type // WARNING: in.VMExtensions requires manual conversion: does not exist in peer-type + // WARNING: in.NetworkInterfaces requires manual conversion: does not exist in peer-type return nil } diff --git a/exp/api/v1alpha4/azuremachinepool_conversion.go b/exp/api/v1alpha4/azuremachinepool_conversion.go index 537a95878b5..c68047ad3b9 100644 --- a/exp/api/v1alpha4/azuremachinepool_conversion.go +++ b/exp/api/v1alpha4/azuremachinepool_conversion.go @@ -41,6 +41,10 @@ func (src *AzureMachinePool) ConvertTo(dstRaw conversion.Hub) error { return err } + if restored.Spec.Template.NetworkInterfaces != nil { + dst.Spec.Template.NetworkInterfaces = restored.Spec.Template.NetworkInterfaces + } + if restored.Spec.Template.Image != nil && restored.Spec.Template.Image.ComputeGallery != nil { dst.Spec.Template.Image.ComputeGallery = restored.Spec.Template.Image.ComputeGallery } @@ -71,6 +75,10 @@ func (dst *AzureMachinePool) ConvertFrom(srcRaw conversion.Hub) error { return err } + if err := utilconversion.MarshalData(src, dst); err != nil { + return err + } + // Preserve Hub data on down-conversion. return utilconversion.MarshalData(src, dst) } diff --git a/exp/api/v1alpha4/azuremachinepoolmachine_conversion.go b/exp/api/v1alpha4/azuremachinepoolmachine_conversion.go index 307c3634417..cecfdc1915f 100644 --- a/exp/api/v1alpha4/azuremachinepoolmachine_conversion.go +++ b/exp/api/v1alpha4/azuremachinepoolmachine_conversion.go @@ -18,13 +18,23 @@ package v1alpha4 import ( infrav1exp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1beta1" + utilconversion "sigs.k8s.io/cluster-api/util/conversion" "sigs.k8s.io/controller-runtime/pkg/conversion" ) // ConvertTo converts this AzureMachinePoolMachine to the Hub version (v1beta1). func (src *AzureMachinePoolMachine) ConvertTo(dstRaw conversion.Hub) error { dst := dstRaw.(*infrav1exp.AzureMachinePoolMachine) - return Convert_v1alpha4_AzureMachinePoolMachine_To_v1beta1_AzureMachinePoolMachine(src, dst, nil) + if err := Convert_v1alpha4_AzureMachinePoolMachine_To_v1beta1_AzureMachinePoolMachine(src, dst, nil); err != nil { + return err + } + restored := &infrav1exp.AzureMachinePoolMachine{} + if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok { + return err + } + dst.Spec = restored.Spec + + return nil } // ConvertFrom converts from the Hub version (v1beta1) to this version. diff --git a/exp/api/v1alpha4/zz_generated.conversion.go b/exp/api/v1alpha4/zz_generated.conversion.go index 0d1d238f5d1..6bb6b329dee 100644 --- a/exp/api/v1alpha4/zz_generated.conversion.go +++ b/exp/api/v1alpha4/zz_generated.conversion.go @@ -743,6 +743,7 @@ func autoConvert_v1beta1_AzureMachinePoolMachineTemplate_To_v1alpha4_AzureMachin } out.SubnetName = in.SubnetName // WARNING: in.VMExtensions requires manual conversion: does not exist in peer-type + // WARNING: in.NetworkInterfaces requires manual conversion: does not exist in peer-type return nil } diff --git a/exp/api/v1beta1/azuremachinepool_default.go b/exp/api/v1beta1/azuremachinepool_default.go index 7da43c31503..ece0b4ac026 100644 --- a/exp/api/v1beta1/azuremachinepool_default.go +++ b/exp/api/v1beta1/azuremachinepool_default.go @@ -74,3 +74,31 @@ func (amp *AzureMachinePool) SetDiagnosticsDefaults() { amp.Spec.Template.Diagnostics.Boot = bootDefault } } + +// SetNetworkInterfacesDefaults sets the defaults for the network interfaces. +func (amp *AzureMachinePool) SetNetworkInterfacesDefaults() { + // Ensure the deprecated fields and new fields are not populated simultaneously + if (amp.Spec.Template.SubnetName != "" || amp.Spec.Template.AcceleratedNetworking != nil) && len(amp.Spec.Template.NetworkInterfaces) > 0 { + // Both the deprecated and the new fields are both set, return without changes + // and reject the request in the validating webhook which runs later. + return + } + + if len(amp.Spec.Template.NetworkInterfaces) == 0 { + amp.Spec.Template.NetworkInterfaces = []infrav1.NetworkInterface{ + { + SubnetName: amp.Spec.Template.SubnetName, + AcceleratedNetworking: amp.Spec.Template.AcceleratedNetworking, + }, + } + amp.Spec.Template.SubnetName = "" + amp.Spec.Template.AcceleratedNetworking = nil + } + + // Ensure that PrivateIPConfigs defaults to 1 if not specified. + for i := 0; i < len(amp.Spec.Template.NetworkInterfaces); i++ { + if amp.Spec.Template.NetworkInterfaces[i].PrivateIPConfigs == 0 { + amp.Spec.Template.NetworkInterfaces[i].PrivateIPConfigs = 1 + } + } +} diff --git a/exp/api/v1beta1/azuremachinepool_default_test.go b/exp/api/v1beta1/azuremachinepool_default_test.go index 2719a48f73f..a7b982a349d 100644 --- a/exp/api/v1beta1/azuremachinepool_default_test.go +++ b/exp/api/v1beta1/azuremachinepool_default_test.go @@ -19,6 +19,7 @@ package v1beta1 import ( "testing" + "github.com/Azure/go-autorest/autorest/to" "github.com/google/uuid" . "github.com/onsi/gomega" infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" @@ -143,6 +144,99 @@ func TestAzureMachinePool_SetDiagnosticsDefaults(t *testing.T) { g.Expect(nilDiagnostics.machinePool.Spec.Template.Diagnostics.Boot.StorageAccountType).To(Equal(infrav1.ManagedDiagnosticsStorage)) } +func TestAzureMachinePool_SetNetworkInterfacesDefaults(t *testing.T) { + g := NewWithT(t) + + testCases := []struct { + name string + machinePool *AzureMachinePool + want *AzureMachinePool + }{ + { + name: "defaulting webhook updates MachinePool with deprecated subnetName field", + machinePool: &AzureMachinePool{ + Spec: AzureMachinePoolSpec{ + Template: AzureMachinePoolMachineTemplate{ + SubnetName: "test-subnet", + }, + }, + }, + want: &AzureMachinePool{ + Spec: AzureMachinePoolSpec{ + Template: AzureMachinePoolMachineTemplate{ + SubnetName: "", + NetworkInterfaces: []infrav1.NetworkInterface{ + { + SubnetName: "test-subnet", + PrivateIPConfigs: 1, + }, + }, + }, + }, + }, + }, + { + name: "defaulting webhook updates MachinePool with deprecated acceleratedNetworking field", + machinePool: &AzureMachinePool{ + Spec: AzureMachinePoolSpec{ + Template: AzureMachinePoolMachineTemplate{ + SubnetName: "test-subnet", + AcceleratedNetworking: to.BoolPtr(true), + }, + }, + }, + want: &AzureMachinePool{ + Spec: AzureMachinePoolSpec{ + Template: AzureMachinePoolMachineTemplate{ + SubnetName: "", + AcceleratedNetworking: nil, + NetworkInterfaces: []infrav1.NetworkInterface{ + { + SubnetName: "test-subnet", + PrivateIPConfigs: 1, + AcceleratedNetworking: to.BoolPtr(true), + }, + }, + }, + }, + }, + }, + { + name: "defaulting webhook does nothing if both new and deprecated subnetName fields are set", + machinePool: &AzureMachinePool{ + Spec: AzureMachinePoolSpec{ + Template: AzureMachinePoolMachineTemplate{ + SubnetName: "test-subnet", + NetworkInterfaces: []infrav1.NetworkInterface{{ + SubnetName: "test-subnet", + }}, + }, + }, + }, + want: &AzureMachinePool{ + Spec: AzureMachinePoolSpec{ + Template: AzureMachinePoolMachineTemplate{ + SubnetName: "test-subnet", + AcceleratedNetworking: nil, + NetworkInterfaces: []infrav1.NetworkInterface{ + { + SubnetName: "test-subnet", + }, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.machinePool.SetNetworkInterfacesDefaults() + g.Expect(tc.machinePool).To(Equal(tc.want)) + }) + } +} + func createMachinePoolWithSSHPublicKey(sshPublicKey string) *AzureMachinePool { return hardcodedAzureMachinePoolWithSSHKey(sshPublicKey) } diff --git a/exp/api/v1beta1/azuremachinepool_types.go b/exp/api/v1beta1/azuremachinepool_types.go index b8f64aeba44..d9c0e50fa7a 100644 --- a/exp/api/v1beta1/azuremachinepool_types.go +++ b/exp/api/v1beta1/azuremachinepool_types.go @@ -65,9 +65,7 @@ type ( // SSHPublicKey is the SSH public key string base64 encoded to add to a Virtual Machine SSHPublicKey string `json:"sshPublicKey"` - // AcceleratedNetworking enables or disables Azure accelerated networking. If omitted, it will be set based on - // whether the requested VMSize supports accelerated networking. - // If AcceleratedNetworking is set to true with a VMSize that does not support it, Azure will return an error. + // Deprecated: AcceleratedNetworking should be set in the networkInterfaces field. // +optional AcceleratedNetworking *bool `json:"acceleratedNetworking,omitempty"` @@ -89,13 +87,20 @@ type ( // +optional SpotVMOptions *infrav1.SpotVMOptions `json:"spotVMOptions,omitempty"` - // SubnetName selects the Subnet where the VMSS will be placed + // Deprecated: SubnetName should be set in the networkInterfaces field. // +optional SubnetName string `json:"subnetName,omitempty"` // VMExtensions specifies a list of extensions to be added to the scale set. // +optional VMExtensions []infrav1.VMExtension `json:"vmExtensions,omitempty"` + + // NetworkInterfaces specifies a list of network interface configurations. + // If left unspecified, the VM will get a single network interface with a + // single IPConfig in the subnet specified in the cluster's node subnet field. + // The primary interface will be the first networkInterface specified (index 0) in the list. + // +optional + NetworkInterfaces []infrav1.NetworkInterface `json:"networkInterfaces,omitempty"` } // AzureMachinePoolSpec defines the desired state of AzureMachinePool. diff --git a/exp/api/v1beta1/azuremachinepool_webhook.go b/exp/api/v1beta1/azuremachinepool_webhook.go index 4e47bb6ab16..526dbd5fb45 100644 --- a/exp/api/v1beta1/azuremachinepool_webhook.go +++ b/exp/api/v1beta1/azuremachinepool_webhook.go @@ -50,6 +50,7 @@ func (amp *AzureMachinePool) Default() { } amp.SetIdentityDefaults() amp.SetDiagnosticsDefaults() + amp.SetNetworkInterfacesDefaults() } // +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-azuremachinepool,mutating=false,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=azuremachinepools,versions=v1beta1,name=validation.azuremachinepool.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 @@ -89,6 +90,7 @@ func (amp *AzureMachinePool) Validate(old runtime.Object) error { amp.ValidateDiagnostics, amp.ValidateStrategy(), amp.ValidateSystemAssignedIdentity(old), + amp.ValidateNetwork, } var errs []error @@ -101,6 +103,14 @@ func (amp *AzureMachinePool) Validate(old runtime.Object) error { return kerrors.NewAggregate(errs) } +// ValidateNetwork of an AzureMachinePool. +func (amp *AzureMachinePool) ValidateNetwork() error { + if (amp.Spec.Template.NetworkInterfaces != nil) && len(amp.Spec.Template.NetworkInterfaces) > 0 && amp.Spec.Template.SubnetName != "" { + return errors.New("cannot set both NetworkInterfaces and machine SubnetName") + } + return nil +} + // ValidateImage of an AzureMachinePool. func (amp *AzureMachinePool) ValidateImage() error { if amp.Spec.Template.Image != nil { diff --git a/exp/api/v1beta1/azuremachinepool_webhook_test.go b/exp/api/v1beta1/azuremachinepool_webhook_test.go index fa70a1ec858..b6acac14b3c 100644 --- a/exp/api/v1beta1/azuremachinepool_webhook_test.go +++ b/exp/api/v1beta1/azuremachinepool_webhook_test.go @@ -169,6 +169,21 @@ func TestAzureMachinePool_ValidateCreate(t *testing.T) { }), wantErr: false, }, + { + name: "azuremachinepool with valid legacy network configuration", + amp: createMachinePoolWithNetworkConfig("testSubnet", []infrav1.NetworkInterface{}), + wantErr: false, + }, + { + name: "azuremachinepool with invalid legacy network configuration", + amp: createMachinePoolWithNetworkConfig("testSubnet", []infrav1.NetworkInterface{{SubnetName: "testSubnet"}}), + wantErr: true, + }, + { + name: "azuremachinepool with valid networkinterface configuration", + amp: createMachinePoolWithNetworkConfig("", []infrav1.NetworkInterface{{SubnetName: "testSubnet"}}), + wantErr: false, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -248,6 +263,24 @@ func TestAzureMachinePool_ValidateUpdate(t *testing.T) { }), wantErr: false, }, + { + name: "azuremachinepool with valid network interface config", + oldAMP: createMachinePoolWithNetworkConfig("", []infrav1.NetworkInterface{{SubnetName: "testSubnet"}}), + amp: createMachinePoolWithNetworkConfig("", []infrav1.NetworkInterface{{SubnetName: "testSubnet2"}}), + wantErr: false, + }, + { + name: "azuremachinepool with valid network interface config", + oldAMP: createMachinePoolWithNetworkConfig("", []infrav1.NetworkInterface{{SubnetName: "testSubnet"}}), + amp: createMachinePoolWithNetworkConfig("subnet", []infrav1.NetworkInterface{{SubnetName: "testSubnet2"}}), + wantErr: true, + }, + { + name: "azuremachinepool with valid network interface config", + oldAMP: createMachinePoolWithNetworkConfig("subnet", []infrav1.NetworkInterface{}), + amp: createMachinePoolWithNetworkConfig("subnet", []infrav1.NetworkInterface{{SubnetName: "testSubnet2"}}), + wantErr: true, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -347,6 +380,17 @@ func createMachinePoolWithSharedImage(subscriptionID, resourceGroup, name, galle } } +func createMachinePoolWithNetworkConfig(subnetName string, interfaces []infrav1.NetworkInterface) *AzureMachinePool { + return &AzureMachinePool{ + Spec: AzureMachinePoolSpec{ + Template: AzureMachinePoolMachineTemplate{ + SubnetName: subnetName, + NetworkInterfaces: interfaces, + }, + }, + } +} + func createMachinePoolWithImageByID(imageID string, terminateNotificationTimeout *int) *AzureMachinePool { image := infrav1.Image{ ID: &imageID, diff --git a/exp/api/v1beta1/zz_generated.deepcopy.go b/exp/api/v1beta1/zz_generated.deepcopy.go index 79ac9e8031a..17bea0d6a65 100644 --- a/exp/api/v1beta1/zz_generated.deepcopy.go +++ b/exp/api/v1beta1/zz_generated.deepcopy.go @@ -376,6 +376,13 @@ func (in *AzureMachinePoolMachineTemplate) DeepCopyInto(out *AzureMachinePoolMac (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.NetworkInterfaces != nil { + in, out := &in.NetworkInterfaces, &out.NetworkInterfaces + *out = make([]apiv1beta1.NetworkInterface, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureMachinePoolMachineTemplate. diff --git a/exp/controllers/azuremachinepool_reconciler.go b/exp/controllers/azuremachinepool_reconciler.go index 86416076fb8..ee48e23af59 100644 --- a/exp/controllers/azuremachinepool_reconciler.go +++ b/exp/controllers/azuremachinepool_reconciler.go @@ -57,6 +57,9 @@ func (s *azureMachinePoolService) Reconcile(ctx context.Context) error { ctx, _, done := tele.StartSpanWithLogger(ctx, "controllers.azureMachinePoolService.Reconcile") defer done() + // Ensure that the deprecated networking field values have been migrated to the new NetworkInterfaces field. + s.scope.AzureMachinePool.SetNetworkInterfacesDefaults() + if err := s.scope.SetSubnetName(); err != nil { return errors.Wrap(err, "failed defaulting subnet name") } diff --git a/test/e2e/aks_versions.go b/test/e2e/aks_versions.go index 933e43b0c07..1952d220932 100644 --- a/test/e2e/aks_versions.go +++ b/test/e2e/aks_versions.go @@ -28,11 +28,10 @@ import ( . "github.com/onsi/gomega" "github.com/pkg/errors" "golang.org/x/mod/semver" + azureutil "sigs.k8s.io/cluster-api-provider-azure/util/azure" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/test/framework/clusterctl" "sigs.k8s.io/controller-runtime/pkg/client" - - azureutil "sigs.k8s.io/cluster-api-provider-azure/util/azure" ) // GetAKSKubernetesVersion gets the kubernetes version for AKS clusters as specified by the environment variable defined by versionVar. diff --git a/util/azure/azure.go b/util/azure/azure.go index 24ddd67bd80..4d8f90d329b 100644 --- a/util/azure/azure.go +++ b/util/azure/azure.go @@ -60,7 +60,7 @@ func getCloudConfig(environment azure.Environment) cloud.Configuration { return config } -// GetAuthorizer returns an autorest.Authorizer-compatible object from MSAL +// GetAuthorizer returns an autorest.Authorizer-compatible object from MSAL. func GetAuthorizer(settings auth.EnvironmentSettings) (autorest.Authorizer, error) { // azidentity uses different envvars for certificate authentication: // azidentity: AZURE_CLIENT_CERTIFICATE_{PATH,PASSWORD}