diff --git a/api/v1alpha3/azuremachine_conversion.go b/api/v1alpha3/azuremachine_conversion.go index 5f48457cec2..db4636ca6eb 100644 --- a/api/v1alpha3/azuremachine_conversion.go +++ b/api/v1alpha3/azuremachine_conversion.go @@ -50,6 +50,11 @@ func (src *AzureMachine) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Image.SharedGallery.SKU = restored.Spec.Image.SharedGallery.SKU } + + if restored.Spec.NetworkInterfaces != nil { + dst.Spec.NetworkInterfaces = restored.Spec.NetworkInterfaces + } + if dst.Spec.Image != nil && restored.Spec.Image.ComputeGallery != nil { dst.Spec.Image.ComputeGallery = restored.Spec.Image.ComputeGallery } diff --git a/api/v1alpha3/azuremachinetemplate_conversion.go b/api/v1alpha3/azuremachinetemplate_conversion.go index f2ceb54ddbd..43ab04f3d2e 100644 --- a/api/v1alpha3/azuremachinetemplate_conversion.go +++ b/api/v1alpha3/azuremachinetemplate_conversion.go @@ -50,6 +50,9 @@ func (src *AzureMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Spec.Image.SharedGallery.SKU = restored.Spec.Template.Spec.Image.SharedGallery.SKU } + if restored.Spec.Template.Spec.NetworkInterfaces != nil { + dst.Spec.Template.Spec.NetworkInterfaces = restored.Spec.Template.Spec.NetworkInterfaces + } if dst.Spec.Template.Spec.Image != nil && restored.Spec.Template.Spec.Image.ComputeGallery != nil { dst.Spec.Template.Spec.Image.ComputeGallery = restored.Spec.Template.Spec.Image.ComputeGallery } diff --git a/api/v1alpha3/zz_generated.conversion.go b/api/v1alpha3/zz_generated.conversion.go index 0325968d850..8389016dfee 100644 --- a/api/v1alpha3/zz_generated.conversion.go +++ b/api/v1alpha3/zz_generated.conversion.go @@ -905,6 +905,7 @@ func autoConvert_v1beta1_AzureMachineSpec_To_v1alpha3_AzureMachineSpec(in *v1bet out.SpotVMOptions = (*SpotVMOptions)(unsafe.Pointer(in.SpotVMOptions)) out.SecurityProfile = (*SecurityProfile)(unsafe.Pointer(in.SecurityProfile)) // WARNING: in.SubnetName 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 c8cf37e76fa..9e5b1f2afed 100644 --- a/api/v1alpha4/azuremachine_conversion.go +++ b/api/v1alpha4/azuremachine_conversion.go @@ -36,10 +36,14 @@ func (src *AzureMachine) ConvertTo(dstRaw conversion.Hub) error { return err } + + if restored.Spec.NetworkInterfaces != nil { + dst.Spec.NetworkInterfaces = restored.Spec.NetworkInterfaces + } + if restored.Spec.Image != nil && restored.Spec.Image.ComputeGallery != nil { dst.Spec.Image.ComputeGallery = restored.Spec.Image.ComputeGallery } - return nil } @@ -50,6 +54,10 @@ func (dst *AzureMachine) 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) } @@ -66,6 +74,10 @@ func (dst *AzureMachineList) ConvertFrom(srcRaw conversion.Hub) error { return Convert_v1beta1_AzureMachineList_To_v1alpha4_AzureMachineList(src, dst, nil) } +func Convert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec(in *v1beta1.AzureMachineSpec, out *AzureMachineSpec, s apiconversion.Scope) error { + return autoConvert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec(in, out, s) +} + func Convert_v1beta1_AzureMarketplaceImage_To_v1alpha4_AzureMarketplaceImage(in *v1beta1.AzureMarketplaceImage, out *AzureMarketplaceImage, s apiconversion.Scope) error { out.Offer = in.ImagePlan.Offer out.Publisher = in.ImagePlan.Publisher diff --git a/api/v1alpha4/azuremachinetemplate_conversion.go b/api/v1alpha4/azuremachinetemplate_conversion.go index 3b7e4e8ce9d..d93c20c87c2 100644 --- a/api/v1alpha4/azuremachinetemplate_conversion.go +++ b/api/v1alpha4/azuremachinetemplate_conversion.go @@ -26,16 +26,21 @@ import ( // ConvertTo converts this AzureMachineTemplate to the Hub version (v1beta1). func (src *AzureMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst := dstRaw.(*infrav1beta1.AzureMachineTemplate) - if err := Convert_v1alpha4_AzureMachineTemplate_To_v1beta1_AzureMachineTemplate(src, dst, nil); err != nil { + + if err := autoConvert_v1alpha4_AzureMachineTemplate_To_v1beta1_AzureMachineTemplate(src, dst, nil); err != nil { return err } - // Restore missing fields from annotations restored := &infrav1beta1.AzureMachineTemplate{} if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok { return err } + + if restored.Spec.Template.Spec.NetworkInterfaces != nil { + dst.Spec.Template.Spec.NetworkInterfaces = restored.Spec.Template.Spec.NetworkInterfaces + } + if dst.Spec.Template.Spec.Image != nil && restored.Spec.Template.Spec.Image.ComputeGallery != nil { dst.Spec.Template.Spec.Image.ComputeGallery = restored.Spec.Template.Spec.Image.ComputeGallery } diff --git a/api/v1alpha4/zz_generated.conversion.go b/api/v1alpha4/zz_generated.conversion.go index daed97869ed..15cde92c04a 100644 --- a/api/v1alpha4/zz_generated.conversion.go +++ b/api/v1alpha4/zz_generated.conversion.go @@ -167,11 +167,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta1.AzureMachineSpec)(nil), (*AzureMachineSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec(a.(*v1beta1.AzureMachineSpec), b.(*AzureMachineSpec), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*AzureMachineStatus)(nil), (*v1beta1.AzureMachineStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha4_AzureMachineStatus_To_v1beta1_AzureMachineStatus(a.(*AzureMachineStatus), b.(*v1beta1.AzureMachineStatus), scope) }); err != nil { @@ -462,6 +457,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1beta1.AzureMachineSpec)(nil), (*AzureMachineSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec(a.(*v1beta1.AzureMachineSpec), b.(*AzureMachineSpec), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*v1beta1.AzureMachineTemplateResource)(nil), (*AzureMachineTemplateResource)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_AzureMachineTemplateResource_To_v1alpha4_AzureMachineTemplateResource(a.(*v1beta1.AzureMachineTemplateResource), b.(*AzureMachineTemplateResource), scope) }); err != nil { @@ -1052,14 +1052,10 @@ func autoConvert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec(in *v1bet out.SpotVMOptions = (*SpotVMOptions)(unsafe.Pointer(in.SpotVMOptions)) out.SecurityProfile = (*SecurityProfile)(unsafe.Pointer(in.SecurityProfile)) out.SubnetName = in.SubnetName + // WARNING: in.NetworkInterfaces requires manual conversion: does not exist in peer-type return nil } -// Convert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec is an autogenerated conversion function. -func Convert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec(in *v1beta1.AzureMachineSpec, out *AzureMachineSpec, s conversion.Scope) error { - return autoConvert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec(in, out, s) -} - func autoConvert_v1alpha4_AzureMachineStatus_To_v1beta1_AzureMachineStatus(in *AzureMachineStatus, out *v1beta1.AzureMachineStatus, s conversion.Scope) error { out.Ready = in.Ready out.Addresses = *(*[]corev1.NodeAddress)(unsafe.Pointer(&in.Addresses)) diff --git a/api/v1beta1/azuremachine_types.go b/api/v1beta1/azuremachine_types.go index 40af504fe63..39fe2f619f0 100644 --- a/api/v1beta1/azuremachine_types.go +++ b/api/v1beta1/azuremachine_types.go @@ -114,6 +114,8 @@ type AzureMachineSpec struct { // SubnetName selects the Subnet where the VM will be placed // +optional SubnetName string `json:"subnetName,omitempty"` + + NetworkInterfaces []AzureNetworkInterface `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 7dab64472f6..20782b3ceb9 100644 --- a/api/v1beta1/azuremachine_validation.go +++ b/api/v1beta1/azuremachine_validation.go @@ -54,9 +54,20 @@ func ValidateAzureMachineSpec(spec AzureMachineSpec) field.ErrorList { allErrs = append(allErrs, errs...) } + if errs := ValidateNetwork(spec.SubnetName, spec.NetworkInterfaces, field.NewPath("networkInterfaces")); len(errs) > 0 { + allErrs = append(allErrs, errs...) + } + return allErrs } +func ValidateNetwork(subnetName string, networkInterfaces []AzureNetworkInterface, 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")} + } + 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_webhook.go b/api/v1beta1/azuremachine_webhook.go index d27cea2774e..fd430f89e70 100644 --- a/api/v1beta1/azuremachine_webhook.go +++ b/api/v1beta1/azuremachine_webhook.go @@ -136,6 +136,20 @@ func (m *AzureMachine) ValidateUpdate(oldRaw runtime.Object) error { ) } + if !reflect.DeepEqual(m.Spec.SubnetName, old.Spec.SubnetName) { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "subnetName"), + m.Spec.SecurityProfile, "field is immutable"), + ) + } + + if !reflect.DeepEqual(m.Spec.NetworkInterfaces, old.Spec.NetworkInterfaces) { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "networkInterfaces"), + m.Spec.SecurityProfile, "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 75a5815f212..208116ab557 100644 --- a/api/v1beta1/azuremachine_webhook_test.go +++ b/api/v1beta1/azuremachine_webhook_test.go @@ -103,6 +103,21 @@ func TestAzureMachine_ValidateCreate(t *testing.T) { machine: createMachineWithOsDiskCacheType("invalid_cache_type"), wantErr: true, }, + { + name: "azuremachine with invalid network configuration", + machine: createrMachineWithNetworkConfig("subnet", []AzureNetworkInterface{{SubnetName: "subnet1"}}), + wantErr: true, + }, + { + name: "azuremachine with valid legacy network configuration", + machine: createrMachineWithNetworkConfig("subnet", []AzureNetworkInterface{}), + wantErr: false, + }, + { + name: "azuremachine with valid network configuration", + machine: createrMachineWithNetworkConfig("", []AzureNetworkInterface{{SubnetName: "subnet"}}), + wantErr: false, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -509,6 +524,62 @@ func TestAzureMachine_ValidateUpdate(t *testing.T) { }, wantErr: false, }, + { + name: "invalidTest: azuremachine.spec.subnetName is immutable", + oldMachine: &AzureMachine{ + Spec: AzureMachineSpec{ + SubnetName: "subnet1", + }, + }, + newMachine: &AzureMachine{ + Spec: AzureMachineSpec{ + SubnetName: "subnet2", + }, + }, + wantErr: true, + }, + { + name: "invalidTest: azuremachine.spec.subnetName is immutable", + oldMachine: &AzureMachine{ + Spec: AzureMachineSpec{ + SubnetName: "subnet1", + }, + }, + newMachine: &AzureMachine{ + Spec: AzureMachineSpec{ + SubnetName: "subnet2", + }, + }, + wantErr: true, + }, + { + name: "validTest: azuremachine.spec.networkInterfaces is immutable", + oldMachine: &AzureMachine{ + Spec: AzureMachineSpec{ + NetworkInterfaces: []AzureNetworkInterface{{SubnetName: "subnet"}}, + }, + }, + newMachine: &AzureMachine{ + Spec: AzureMachineSpec{ + NetworkInterfaces: []AzureNetworkInterface{{SubnetName: "subnet"}}, + }, + }, + wantErr: false, + }, + { + name: "invalidtest: azuremachine.spec.networkInterfaces is immutable", + oldMachine: &AzureMachine{ + Spec: AzureMachineSpec{ + NetworkInterfaces: []AzureNetworkInterface{{SubnetName: "subnet1"}}, + }, + }, + newMachine: &AzureMachine{ + Spec: AzureMachineSpec{ + NetworkInterfaces: []AzureNetworkInterface{{SubnetName: "subnet2"}}, + }, + }, + wantErr: true, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -550,6 +621,17 @@ func TestAzureMachine_Default(t *testing.T) { } } +func createrMachineWithNetworkConfig(subnetName string, interfaces []AzureNetworkInterface) *AzureMachine { + return &AzureMachine{ + Spec: AzureMachineSpec{ + SubnetName: subnetName, + NetworkInterfaces: interfaces, + 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 060cbf4eb4b..701fbd48f00 100644 --- a/api/v1beta1/azuremachinetemplate_webhook.go +++ b/api/v1beta1/azuremachinetemplate_webhook.go @@ -57,6 +57,10 @@ func (r *AzureMachineTemplate) ValidateCreate() error { ) } + 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 } diff --git a/api/v1beta1/types.go b/api/v1beta1/types.go index 7907eecf493..df5e3ea8047 100644 --- a/api/v1beta1/types.go +++ b/api/v1beta1/types.go @@ -579,6 +579,22 @@ type SubnetSpec struct { SubnetClassSpec `json:",inline"` } +// Network Interfaces to attach to each VM +// +optional +type AzureNetworkInterface struct { + SubnetName string `json:"subnetName,omitempty"` + IPConfigs []AzureIPConfig `json:"ipConfigs,omitempty"` + AcceleratedNetworking *bool `json:"acceleratedNetworking,omitempty"` + ID string `json:"id,omitempty"` +} + +// IP Configuration defines options to confiure a network interface. +type AzureIPConfig struct { + PrivateIP string `json:"privateIP,omitempty"` + PublicIP bool `json:"publicIP,omitempty"` + PublicIPAddress string `json:"publicIPAddress,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 2516b5c557b..05d73c28a59 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -486,6 +486,21 @@ func (in *AzureComputeGalleryImage) DeepCopy() *AzureComputeGalleryImage { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AzureIPConfig) DeepCopyInto(out *AzureIPConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureIPConfig. +func (in *AzureIPConfig) DeepCopy() *AzureIPConfig { + if in == nil { + return nil + } + out := new(AzureIPConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AzureMachine) DeepCopyInto(out *AzureMachine) { *out = *in @@ -598,6 +613,13 @@ func (in *AzureMachineSpec) DeepCopyInto(out *AzureMachineSpec) { *out = new(SecurityProfile) (*in).DeepCopyInto(*out) } + if in.NetworkInterfaces != nil { + in, out := &in.NetworkInterfaces, &out.NetworkInterfaces + *out = make([]AzureNetworkInterface, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureMachineSpec. @@ -764,6 +786,31 @@ func (in *AzureMarketplaceImage) DeepCopy() *AzureMarketplaceImage { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AzureNetworkInterface) DeepCopyInto(out *AzureNetworkInterface) { + *out = *in + if in.IPConfigs != nil { + in, out := &in.IPConfigs, &out.IPConfigs + *out = make([]AzureIPConfig, len(*in)) + copy(*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 AzureNetworkInterface. +func (in *AzureNetworkInterface) DeepCopy() *AzureNetworkInterface { + if in == nil { + return nil + } + out := new(AzureNetworkInterface) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AzureSharedGalleryImage) DeepCopyInto(out *AzureSharedGalleryImage) { *out = *in diff --git a/azure/scope/machine.go b/azure/scope/machine.go index be5b837705e..742e3b091b0 100644 --- a/azure/scope/machine.go +++ b/azure/scope/machine.go @@ -20,6 +20,7 @@ import ( "context" "encoding/base64" "encoding/json" + "strconv" "strings" "time" @@ -220,47 +221,109 @@ func (m *MachineScope) InboundNatSpecs(portsInUse map[int32]struct{}) []azure.Re // NICSpecs returns the network interface specs. func (m *MachineScope) NICSpecs() []azure.ResourceSpecGetter { - spec := &networkinterfaces.NICSpec{ - Name: azure.GenerateNICName(m.Name()), - 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, - IPv6Enabled: m.IsIPv6Enabled(), - EnableIPForwarding: m.AzureMachine.Spec.EnableIPForwarding, - } + nicSpecs := []azure.ResourceSpecGetter{} + + if len(m.AzureMachine.Spec.NetworkInterfaces) < 1 { + spec := &networkinterfaces.NICSpec{ + Name: azure.GenerateNICName(m.Name()), + ResourceGroup: m.ResourceGroup(), + Location: m.Location(), + SubscriptionID: m.SubscriptionID(), + MachineName: m.Name(), + VNetName: m.Vnet().Name, + VNetResourceGroup: m.Vnet().ResourceGroup, + AcceleratedNetworking: m.AzureMachine.Spec.AcceleratedNetworking, + IPv6Enabled: m.IsIPv6Enabled(), + EnableIPForwarding: m.AzureMachine.Spec.EnableIPForwarding, + SubnetName: m.Subnet().Name, + } + 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.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 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 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 m.cache != nil { + spec.SKU = &m.cache.VMSKU + } - if m.Role() == infrav1.Node && m.AzureMachine.Spec.AllocatePublicIP { - spec.PublicIPName = azure.GenerateNodePublicIPName(m.Name()) + if m.Role() == infrav1.Node && m.AzureMachine.Spec.AllocatePublicIP { + spec.PublicIPName = azure.GenerateNodePublicIPName(m.Name()) + } + return append(nicSpecs, spec) } - if m.cache != nil { - spec.SKU = &m.cache.VMSKU - } + for i, n := range m.AzureMachine.Spec.NetworkInterfaces { + if n.ID != "" { + continue + } + spec := &networkinterfaces.NICSpec{ + Name: azure.GenerateNICName(m.Name()) + "-" + strconv.Itoa(i), + ResourceGroup: m.ResourceGroup(), + Location: m.Location(), + SubscriptionID: m.SubscriptionID(), + MachineName: m.Name(), + VNetName: m.Vnet().Name, + VNetResourceGroup: m.Vnet().ResourceGroup, + AcceleratedNetworking: n.AcceleratedNetworking, + IPv6Enabled: m.IsIPv6Enabled(), + EnableIPForwarding: m.AzureMachine.Spec.EnableIPForwarding, + IPConfigs: []networkinterfaces.IPConfig{}, + } - return []azure.ResourceSpecGetter{spec} + spec.SubnetName = n.SubnetName + + // Check for control plane interface setup on interface 0 + if m.Role() == infrav1.ControlPlane && i == 0 { + 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 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 && i == 0 { + spec.PublicLBName = m.OutboundLBName(m.Role()) + spec.PublicLBAddressPoolName = m.OutboundPoolName(m.OutboundLBName(m.Role())) + } + + // Preserve behavior if AllocatePublicIP is used on interface 0 + if m.Role() == infrav1.Node && m.AzureMachine.Spec.AllocatePublicIP && i == 0 { + spec.PublicIPName = azure.GenerateNodePublicIPName(m.Name()) + } + + if m.cache != nil { + spec.SKU = &m.cache.VMSKU + } + + for _, c := range n.IPConfigs { + config := networkinterfaces.IPConfig{ + PublicIP: c.PublicIP, + PrivateIP: c.PrivateIP, + PublicIPAddress: c.PublicIPAddress, + } + spec.IPConfigs = append(spec.IPConfigs, config) + } + nicSpecs = append(nicSpecs, spec) + } + return nicSpecs } // NICIDs returns the NIC resource IDs. @@ -270,6 +333,11 @@ func (m *MachineScope) NICIDs() []string { for i, nic := range nicspecs { nicIDs[i] = azure.NetworkInterfaceID(m.SubscriptionID(), nic.ResourceGroupName(), nic.ResourceName()) } + for _, n := range m.AzureMachine.Spec.NetworkInterfaces { + if n.ID != "" { + nicIDs = append(nicIDs, n.ID) + } + } return nicIDs } diff --git a/azure/scope/machine_test.go b/azure/scope/machine_test.go index 9195e2ff29c..9171ea265ae 100644 --- a/azure/scope/machine_test.go +++ b/azure/scope/machine_test.go @@ -2078,6 +2078,238 @@ 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.AzureNetworkInterface{ + { + SubnetName: "subnet1", + AcceleratedNetworking: pointer.Bool(true), + IPConfigs: []infrav1.AzureIPConfig{{}}, + }, + { + SubnetName: "subnet2", + AcceleratedNetworking: pointer.Bool(true), + IPConfigs: []infrav1.AzureIPConfig{{}}, + }, + }, + }, + }, + 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, + }, + &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, + }, + }, + }, + { + 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.AzureNetworkInterface{ + { + SubnetName: "subnet1", + AcceleratedNetworking: pointer.Bool(true), + IPConfigs: []infrav1.AzureIPConfig{{}, {}, {}, {}, {}, {}, {}, {}, {}, {}}, + }, + }, + }, + }, + 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, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/azure/scope/machinepool.go b/azure/scope/machinepool.go index d3333d3c94f..acacbcefcd6 100644 --- a/azure/scope/machinepool.go +++ b/azure/scope/machinepool.go @@ -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, } } diff --git a/azure/scope/machinepool_test.go b/azure/scope/machinepool_test.go index 4fc5c144573..eb24527d358 100644 --- a/azure/scope/machinepool_test.go +++ b/azure/scope/machinepool_test.go @@ -97,6 +97,48 @@ func TestMachinePoolScope_Name(t *testing.T) { }) } } +func TestMachinePoolScope_MultipleInterfaces(t *testing.T) { + tests := []struct { + name string + machinePoolScope MachinePoolScope + want int + }{ + { + 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.AzureNetworkInterface{ + { + 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 +351,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 *clusterv1exp.MachinePool, amp *infrav1exp.AzureMachinePool) diff --git a/azure/services/networkinterfaces/networkinterfaces_test.go b/azure/services/networkinterfaces/networkinterfaces_test.go index 4529567bf98..99bb4ae1a47 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: 500}, "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.CreateResource(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 c926f35b3ea..631193aba65 100644 --- a/azure/services/networkinterfaces/spec.go +++ b/azure/services/networkinterfaces/spec.go @@ -17,6 +17,8 @@ limitations under the License. package networkinterfaces import ( + "strconv" + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-02-01/network" "github.com/Azure/go-autorest/autorest/to" "github.com/pkg/errors" @@ -45,6 +47,14 @@ type NICSpec struct { IPv6Enabled bool EnableIPForwarding bool SKU *resourceskus.SKU + IPConfigs []IPConfig + Primary *bool +} + +type IPConfig struct { + PrivateIP string + PublicIP bool + PublicIPAddress string } // ResourceName returns the name of the network interface. @@ -132,6 +142,34 @@ func (s *NICSpec) Parameters(existing interface{}) (parameters interface{}, err }, } + for i, c := range s.IPConfigs { + 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"), @@ -144,6 +182,7 @@ func (s *NICSpec) Parameters(existing interface{}) (parameters interface{}, err ipConfigurations = append(ipConfigurations, ipv6Config) } + ipConfigurations[0].InterfaceIPConfigurationPropertiesFormat.Primary = to.BoolPtr(true) return network.Interface{ Location: to.StringPtr(s.Location), diff --git a/azure/services/networkinterfaces/spec_test.go b/azure/services/networkinterfaces/spec_test.go index 5051d099175..5cc7c3652a2 100644 --- a/azure/services/networkinterfaces/spec_test.go +++ b/azure/services/networkinterfaces/spec_test.go @@ -177,12 +177,14 @@ func TestParameters(t *testing.T) { g.Expect(result.(network.Interface)).To(Equal(network.Interface{ Location: to.StringPtr("fake-location"), InterfacePropertiesFormat: &network.InterfacePropertiesFormat{ + Primary: nil, EnableAcceleratedNetworking: to.BoolPtr(true), EnableIPForwarding: to.BoolPtr(false), 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.IPAllocationMethodStatic, PrivateIPAddress: to.StringPtr("fake.static.ip"), @@ -206,10 +208,12 @@ func TestParameters(t *testing.T) { InterfacePropertiesFormat: &network.InterfacePropertiesFormat{ EnableAcceleratedNetworking: to.BoolPtr(true), EnableIPForwarding: to.BoolPtr(false), + 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")}, @@ -232,10 +236,12 @@ func TestParameters(t *testing.T) { InterfacePropertiesFormat: &network.InterfacePropertiesFormat{ EnableAcceleratedNetworking: to.BoolPtr(true), EnableIPForwarding: to.BoolPtr(false), + 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")}}, @@ -259,12 +265,14 @@ func TestParameters(t *testing.T) { g.Expect(result.(network.Interface)).To(Equal(network.Interface{ Location: to.StringPtr("fake-location"), InterfacePropertiesFormat: &network.InterfacePropertiesFormat{ + Primary: nil, EnableAcceleratedNetworking: to.BoolPtr(true), EnableIPForwarding: to.BoolPtr(false), 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{}, @@ -285,12 +293,14 @@ func TestParameters(t *testing.T) { g.Expect(result.(network.Interface)).To(Equal(network.Interface{ Location: to.StringPtr("fake-location"), InterfacePropertiesFormat: &network.InterfacePropertiesFormat{ + Primary: nil, EnableAcceleratedNetworking: to.BoolPtr(false), EnableIPForwarding: to.BoolPtr(false), 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{}, @@ -311,12 +321,14 @@ func TestParameters(t *testing.T) { g.Expect(result.(network.Interface)).To(Equal(network.Interface{ Location: to.StringPtr("fake-location"), InterfacePropertiesFormat: &network.InterfacePropertiesFormat{ + Primary: nil, EnableAcceleratedNetworking: to.BoolPtr(true), EnableIPForwarding: to.BoolPtr(true), 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{}, diff --git a/azure/services/scalesets/scalesets.go b/azure/services/scalesets/scalesets.go index dafd7706897..340ea2776ea 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" @@ -432,6 +433,7 @@ func (s *Service) buildVMSSFromSpec(ctx context.Context, vmssSpec azure.ScaleSet }, Overprovision: to.BoolPtr(false), VirtualMachineProfile: &compute.VirtualMachineScaleSetVMProfile{ + NetworkProfile: &compute.VirtualMachineScaleSetNetworkProfile{}, OsProfile: osProfile, StorageProfile: storageProfile, SecurityProfile: securityProfile, @@ -440,31 +442,6 @@ func (s *Service) buildVMSSFromSpec(ctx context.Context, vmssSpec azure.ScaleSet Enabled: to.BoolPtr(true), }, }, - NetworkProfile: &compute.VirtualMachineScaleSetNetworkProfile{ - NetworkInterfaceConfigurations: &[]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, - }, - }, - }, - }, Priority: priority, EvictionPolicy: evictionPolicy, BillingProfile: billingProfile, @@ -475,6 +452,97 @@ func (s *Service) buildVMSSFromSpec(ctx context.Context, vmssSpec azure.ScaleSet }, } + // Use custom NIC definitons 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 n.ID != "" { + nicConfig.ID = &n.ID + } else { + if to.Bool(n.AcceleratedNetworking) { + nicConfig.VirtualMachineScaleSetNetworkConfigurationProperties.EnableAcceleratedNetworking = to.BoolPtr(true) + } else { + nicConfig.VirtualMachineScaleSetNetworkConfigurationProperties.EnableAcceleratedNetworking = to.BoolPtr(false) + } + if len(n.IPConfigs) == 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{} + for j := range n.IPConfigs { + ipconfig := compute.VirtualMachineScaleSetIPConfiguration{ + Name: to.StringPtr(fmt.Sprintf("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)), + }, + }, + } + if j == 0 { + ipconfig.Primary = to.BoolPtr(true) + if i == 0 { + // only set Load Balancer Backend Address Pool on primary nic/ipconfig + ipconfig.LoadBalancerBackendAddressPools = &backendAddressPools + } + } else { + ipconfig.Primary = to.BoolPtr(false) + } + ipconfig.Subnet = &compute.APIEntityReference{ + ID: to.StringPtr(azure.SubnetID(s.Scope.SubscriptionID(), vmssSpec.VNetResourceGroup, vmssSpec.VNetName, n.SubnetName)), + } + ipconfigs = append(ipconfigs, ipconfig) + } + nicConfig.VirtualMachineScaleSetNetworkConfigurationProperties.IPConfigurations = &ipconfigs + } + } + if i == 0 { + nicConfig.VirtualMachineScaleSetNetworkConfigurationProperties.Primary = to.BoolPtr(true) + } + nicConfigs = append(nicConfigs, nicConfig) + } + vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.NetworkProfile.NetworkInterfaceConfigurations = &nicConfigs + } else { + // Set default interface configuration if no custom ones are specified + vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.NetworkProfile.NetworkInterfaceConfigurations = &[]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, + }, + }, + } + } + // Assign Identity to VMSS if vmssSpec.Identity == infrav1.VMIdentitySystemAssigned { vmss.Identity = &compute.VirtualMachineScaleSetIdentity{ diff --git a/azure/services/scalesets/scalesets_test.go b/azure/services/scalesets/scalesets_test.go index b252c5e77a1..caa9695dcde 100644 --- a/azure/services/scalesets/scalesets_test.go +++ b/azure/services/scalesets/scalesets_test.go @@ -255,6 +255,79 @@ 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.AzureNetworkInterface{ + { + SubnetName: "my-subnet", + IPConfigs: []infrav1.AzureIPConfig{{}}, + AcceleratedNetworking: pointer.Bool(true), + }, + { + SubnetName: "subnet2", + IPConfigs: []infrav1.AzureIPConfig{{}, {}}, + 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("ipConfig0") + (*nic1IPConfigs)[0].PrivateIPAddressVersion = compute.IPVersionIPv4 + (*netConfigs)[0].EnableAcceleratedNetworking = to.BoolPtr(true) + vmssIPConfigs := []compute.VirtualMachineScaleSetIPConfiguration{ + { + Name: to.StringPtr("ipConfig0"), + 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"), + }, + //LoadBalancerBackendAddressPools: &[]compute.SubResource{{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/loadBalancers/capz-lb/backendAddressPools/backendPool")}}, + }, + }, + { + Name: to.StringPtr("ipConfig1"), + VirtualMachineScaleSetIPConfigurationProperties: &compute.VirtualMachineScaleSetIPConfigurationProperties{ + Primary: to.BoolPtr(false), + PrivateIPAddressVersion: compute.IPVersionIPv4, + Subnet: &compute.APIEntityReference{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/subnet2"), + }, + //LoadBalancerBackendAddressPools: &[]compute.SubResource{{ID: to.StringPtr("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/loadBalancers/capz-lb/backendAddressPools/backendPool")}}, + }, + }, + } + *netConfigs = append(*netConfigs, compute.VirtualMachineScaleSetNetworkConfiguration{ + Name: to.StringPtr("my-vmss-1"), + VirtualMachineScaleSetNetworkConfigurationProperties: &compute.VirtualMachineScaleSetNetworkConfigurationProperties{ + Primary: nil, + 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 3e0ad04199e..a598375063c 100644 --- a/azure/types.go +++ b/azure/types.go @@ -127,6 +127,7 @@ type ScaleSetSpec struct { SecurityProfile *infrav1.SecurityProfile SpotVMOptions *infrav1.SpotVMOptions FailureDomains []string + NetworkInterfaces []infrav1.AzureNetworkInterface } // 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 a45716ba497..1e5bd99e9c6 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepools.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepools.yaml @@ -1705,6 +1705,33 @@ spec: - version type: object type: object + networkInterfaces: + description: Network Interfaces to attach to the to a virtual + machine + items: + description: Network Interfaces to attach to each VM + properties: + acceleratedNetworking: + type: boolean + id: + type: string + ipConfigs: + items: + description: IP Configuration defines options to confiure + a network interface. + properties: + privateIP: + type: string + publicIP: + type: boolean + publicIPAddress: + type: string + type: object + type: array + subnetName: + type: string + type: object + type: array osDisk: description: OSDisk contains the operating system disk information for a Virtual Machine 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 f7fa1fcc67f..8f5fc611b1f 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml @@ -1312,6 +1312,31 @@ spec: - version type: object type: object + networkInterfaces: + items: + description: Network Interfaces to attach to each VM + properties: + acceleratedNetworking: + type: boolean + id: + type: string + ipConfigs: + items: + description: IP Configuration defines options to confiure + a network interface. + properties: + privateIP: + type: string + publicIP: + type: boolean + publicIPAddress: + type: string + type: object + type: array + subnetName: + type: string + type: object + type: array osDisk: description: OSDisk specifies the parameters for the operating system disk of the machine 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 12fe01ab8d6..faae6ad8f2e 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinetemplates.yaml @@ -1094,6 +1094,31 @@ spec: - version type: object type: object + networkInterfaces: + items: + description: Network Interfaces to attach to each VM + properties: + acceleratedNetworking: + type: boolean + id: + type: string + ipConfigs: + items: + description: IP Configuration defines options to confiure + a network interface. + properties: + privateIP: + type: string + publicIP: + type: boolean + publicIPAddress: + type: string + type: object + type: array + subnetName: + type: string + type: object + type: array osDisk: description: OSDisk specifies the parameters for the operating system disk of the machine diff --git a/exp/api/v1alpha3/azuremachinepool_conversion.go b/exp/api/v1alpha3/azuremachinepool_conversion.go index 20d115c4a04..a965afb4a1b 100644 --- a/exp/api/v1alpha3/azuremachinepool_conversion.go +++ b/exp/api/v1alpha3/azuremachinepool_conversion.go @@ -72,8 +72,14 @@ func (src *AzureMachinePool) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Image.SharedGallery.SKU = restored.Spec.Template.Image.SharedGallery.SKU } + + if restored.Spec.Template.NetworkInterfaces != nil { + dst.Spec.Template.NetworkInterfaces = restored.Spec.Template.NetworkInterfaces + } + if dst.Spec.Template.Image != nil && restored.Spec.Template.Image.ComputeGallery != nil { dst.Spec.Template.Image.ComputeGallery = restored.Spec.Template.Image.ComputeGallery + } if len(dst.Annotations) == 0 { diff --git a/exp/api/v1alpha3/zz_generated.conversion.go b/exp/api/v1alpha3/zz_generated.conversion.go index af9e278987c..00ae36ca362 100644 --- a/exp/api/v1alpha3/zz_generated.conversion.go +++ b/exp/api/v1alpha3/zz_generated.conversion.go @@ -459,6 +459,7 @@ func autoConvert_v1beta1_AzureMachinePoolMachineTemplate_To_v1alpha3_AzureMachin out.SecurityProfile = (*clusterapiproviderazureapiv1alpha3.SecurityProfile)(unsafe.Pointer(in.SecurityProfile)) out.SpotVMOptions = (*clusterapiproviderazureapiv1alpha3.SpotVMOptions)(unsafe.Pointer(in.SpotVMOptions)) // WARNING: in.SubnetName 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 e96c9aa6dc3..2bf6318ec29 100644 --- a/exp/api/v1alpha4/azuremachinepool_conversion.go +++ b/exp/api/v1alpha4/azuremachinepool_conversion.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha4 import ( + machineryConversion "k8s.io/apimachinery/pkg/conversion" expv1beta1 "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" @@ -25,6 +26,7 @@ import ( // ConvertTo converts this AzureMachinePool to the Hub version (v1beta1). func (src *AzureMachinePool) ConvertTo(dstRaw conversion.Hub) error { dst := dstRaw.(*expv1beta1.AzureMachinePool) + if err := Convert_v1alpha4_AzureMachinePool_To_v1beta1_AzureMachinePool(src, dst, nil); err != nil { return err } @@ -35,6 +37,11 @@ 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 } @@ -53,6 +60,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) } @@ -68,3 +79,7 @@ func (dst *AzureMachinePoolList) ConvertFrom(srcRaw conversion.Hub) error { src := srcRaw.(*expv1beta1.AzureMachinePoolList) return Convert_v1beta1_AzureMachinePoolList_To_v1alpha4_AzureMachinePoolList(src, dst, nil) } + +func Convert_v1beta1_AzureMachinePoolMachineTemplate_To_v1alpha4_AzureMachinePoolMachineTemplate(in *expv1beta1.AzureMachinePoolMachineTemplate, out *AzureMachinePoolMachineTemplate, s machineryConversion.Scope) error { + return autoConvert_v1beta1_AzureMachinePoolMachineTemplate_To_v1alpha4_AzureMachinePoolMachineTemplate(in, out, s) +} diff --git a/exp/api/v1alpha4/azuremachinepoolmachine_conversion.go b/exp/api/v1alpha4/azuremachinepoolmachine_conversion.go index da278521ff4..2e17d08d3b8 100644 --- a/exp/api/v1alpha4/azuremachinepoolmachine_conversion.go +++ b/exp/api/v1alpha4/azuremachinepoolmachine_conversion.go @@ -18,13 +18,23 @@ package v1alpha4 import ( expv1beta1 "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.(*expv1beta1.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 := &expv1beta1.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 0a9bd141e71..857184a61fc 100644 --- a/exp/api/v1alpha4/zz_generated.conversion.go +++ b/exp/api/v1alpha4/zz_generated.conversion.go @@ -149,11 +149,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta1.AzureMachinePoolMachineTemplate)(nil), (*AzureMachinePoolMachineTemplate)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_AzureMachinePoolMachineTemplate_To_v1alpha4_AzureMachinePoolMachineTemplate(a.(*v1beta1.AzureMachinePoolMachineTemplate), b.(*AzureMachinePoolMachineTemplate), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*AzureMachinePoolSpec)(nil), (*v1beta1.AzureMachinePoolSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha4_AzureMachinePoolSpec_To_v1beta1_AzureMachinePoolSpec(a.(*AzureMachinePoolSpec), b.(*v1beta1.AzureMachinePoolSpec), scope) }); err != nil { @@ -344,6 +339,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1beta1.AzureMachinePoolMachineTemplate)(nil), (*AzureMachinePoolMachineTemplate)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_AzureMachinePoolMachineTemplate_To_v1alpha4_AzureMachinePoolMachineTemplate(a.(*v1beta1.AzureMachinePoolMachineTemplate), b.(*AzureMachinePoolMachineTemplate), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*v1beta1.AzureManagedControlPlaneSpec)(nil), (*AzureManagedControlPlaneSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_AzureManagedControlPlaneSpec_To_v1alpha4_AzureManagedControlPlaneSpec(a.(*v1beta1.AzureManagedControlPlaneSpec), b.(*AzureManagedControlPlaneSpec), scope) }); err != nil { @@ -715,14 +715,10 @@ func autoConvert_v1beta1_AzureMachinePoolMachineTemplate_To_v1alpha4_AzureMachin out.SecurityProfile = (*clusterapiproviderazureapiv1alpha4.SecurityProfile)(unsafe.Pointer(in.SecurityProfile)) out.SpotVMOptions = (*clusterapiproviderazureapiv1alpha4.SpotVMOptions)(unsafe.Pointer(in.SpotVMOptions)) out.SubnetName = in.SubnetName + // WARNING: in.NetworkInterfaces requires manual conversion: does not exist in peer-type return nil } -// Convert_v1beta1_AzureMachinePoolMachineTemplate_To_v1alpha4_AzureMachinePoolMachineTemplate is an autogenerated conversion function. -func Convert_v1beta1_AzureMachinePoolMachineTemplate_To_v1alpha4_AzureMachinePoolMachineTemplate(in *v1beta1.AzureMachinePoolMachineTemplate, out *AzureMachinePoolMachineTemplate, s conversion.Scope) error { - return autoConvert_v1beta1_AzureMachinePoolMachineTemplate_To_v1alpha4_AzureMachinePoolMachineTemplate(in, out, s) -} - func autoConvert_v1alpha4_AzureMachinePoolSpec_To_v1beta1_AzureMachinePoolSpec(in *AzureMachinePoolSpec, out *v1beta1.AzureMachinePoolSpec, s conversion.Scope) error { out.Location = in.Location if err := Convert_v1alpha4_AzureMachinePoolMachineTemplate_To_v1beta1_AzureMachinePoolMachineTemplate(&in.Template, &out.Template, s); err != nil { diff --git a/exp/api/v1beta1/azuremachinepool_types.go b/exp/api/v1beta1/azuremachinepool_types.go index 7f3b6a50546..fe204403fcd 100644 --- a/exp/api/v1beta1/azuremachinepool_types.go +++ b/exp/api/v1beta1/azuremachinepool_types.go @@ -87,6 +87,10 @@ type ( // SubnetName selects the Subnet where the VMSS will be placed // +optional SubnetName string `json:"subnetName,omitempty"` + + // Network Interfaces to attach to the to a virtual machine + // +optional + NetworkInterfaces []infrav1.AzureNetworkInterface `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 368012a1c75..3231ff58b09 100644 --- a/exp/api/v1beta1/azuremachinepool_webhook.go +++ b/exp/api/v1beta1/azuremachinepool_webhook.go @@ -88,6 +88,7 @@ func (amp *AzureMachinePool) Validate(old runtime.Object) error { amp.ValidateUserAssignedIdentity, amp.ValidateStrategy(), amp.ValidateSystemAssignedIdentity(old), + amp.ValidateNetwork, } var errs []error @@ -100,6 +101,13 @@ func (amp *AzureMachinePool) Validate(old runtime.Object) error { return kerrors.NewAggregate(errs) } +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 77e5bc1bf25..0a2a01e4ff5 100644 --- a/exp/api/v1beta1/azuremachinepool_webhook_test.go +++ b/exp/api/v1beta1/azuremachinepool_webhook_test.go @@ -142,6 +142,21 @@ func TestAzureMachinePool_ValidateCreate(t *testing.T) { }), wantErr: false, }, + { + name: "azuremachinepool with valid legacy network configuration", + amp: createMachinePoolWithNetworkConfig("testSubnet", []infrav1.AzureNetworkInterface{}), + wantErr: false, + }, + { + name: "azuremachinepool with invalid legacy network configuration", + amp: createMachinePoolWithNetworkConfig("testSubnet", []infrav1.AzureNetworkInterface{{SubnetName: "testSubnet"}}), + wantErr: true, + }, + { + name: "azuremachinepool with valid networkinterface configuration", + amp: createMachinePoolWithNetworkConfig("", []infrav1.AzureNetworkInterface{{SubnetName: "testSubnet"}}), + wantErr: false, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -221,6 +236,24 @@ func TestAzureMachinePool_ValidateUpdate(t *testing.T) { }), wantErr: false, }, + { + name: "azuremachinepool with valid network interface config", + oldAMP: createMachinePoolWithNetworkConfig("", []infrav1.AzureNetworkInterface{{SubnetName: "testSubnet"}}), + amp: createMachinePoolWithNetworkConfig("", []infrav1.AzureNetworkInterface{{SubnetName: "testSubnet2"}}), + wantErr: false, + }, + { + name: "azuremachinepool with valid network interface config", + oldAMP: createMachinePoolWithNetworkConfig("", []infrav1.AzureNetworkInterface{{SubnetName: "testSubnet"}}), + amp: createMachinePoolWithNetworkConfig("subnet", []infrav1.AzureNetworkInterface{{SubnetName: "testSubnet2"}}), + wantErr: true, + }, + { + name: "azuremachinepool with valid network interface config", + oldAMP: createMachinePoolWithNetworkConfig("subnet", []infrav1.AzureNetworkInterface{}), + amp: createMachinePoolWithNetworkConfig("subnet", []infrav1.AzureNetworkInterface{{SubnetName: "testSubnet2"}}), + wantErr: true, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -320,6 +353,17 @@ func createMachinePoolWithSharedImage(subscriptionID, resourceGroup, name, galle } } +func createMachinePoolWithNetworkConfig(subnetName string, interfaces []infrav1.AzureNetworkInterface) *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 9e35f3cf62f..5d3953ec4a3 100644 --- a/exp/api/v1beta1/zz_generated.deepcopy.go +++ b/exp/api/v1beta1/zz_generated.deepcopy.go @@ -364,6 +364,13 @@ func (in *AzureMachinePoolMachineTemplate) DeepCopyInto(out *AzureMachinePoolMac *out = new(apiv1beta1.SpotVMOptions) (*in).DeepCopyInto(*out) } + if in.NetworkInterfaces != nil { + in, out := &in.NetworkInterfaces, &out.NetworkInterfaces + *out = make([]apiv1beta1.AzureNetworkInterface, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureMachinePoolMachineTemplate.