diff --git a/api/v1alpha3/azurecluster_conversion.go b/api/v1alpha3/azurecluster_conversion.go index f8f743b1e7d..8f3f0bcc029 100644 --- a/api/v1alpha3/azurecluster_conversion.go +++ b/api/v1alpha3/azurecluster_conversion.go @@ -95,6 +95,9 @@ func (src *AzureCluster) ConvertTo(dstRaw conversion.Hub) error { // nolint dst.Status.LongRunningOperationStates = restored.Status.LongRunningOperationStates + // Restore list of virtual network peerings + dst.Spec.NetworkSpec.Vnet.Peerings = restored.Spec.NetworkSpec.Vnet.Peerings + return nil } diff --git a/api/v1alpha3/zz_generated.conversion.go b/api/v1alpha3/zz_generated.conversion.go index 8085aa79f95..18348d4896a 100644 --- a/api/v1alpha3/zz_generated.conversion.go +++ b/api/v1alpha3/zz_generated.conversion.go @@ -1612,6 +1612,7 @@ func autoConvert_v1beta1_VnetSpec_To_v1alpha3_VnetSpec(in *v1beta1.VnetSpec, out out.ID = in.ID out.Name = in.Name out.CIDRBlocks = *(*[]string)(unsafe.Pointer(&in.CIDRBlocks)) + // WARNING: in.Peerings requires manual conversion: does not exist in peer-type out.Tags = *(*Tags)(unsafe.Pointer(&in.Tags)) return nil } diff --git a/api/v1alpha4/azurecluster_conversion.go b/api/v1alpha4/azurecluster_conversion.go index d91f4c01dd5..f97e4e1252f 100644 --- a/api/v1alpha4/azurecluster_conversion.go +++ b/api/v1alpha4/azurecluster_conversion.go @@ -17,6 +17,9 @@ limitations under the License. package v1alpha4 import ( + apiconversion "k8s.io/apimachinery/pkg/conversion" + utilconversion "sigs.k8s.io/cluster-api/util/conversion" + infrav1beta1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" "sigs.k8s.io/controller-runtime/pkg/conversion" ) @@ -24,13 +27,35 @@ import ( // ConvertTo converts this AzureCluster to the Hub version (v1beta1). func (src *AzureCluster) ConvertTo(dstRaw conversion.Hub) error { // nolint dst := dstRaw.(*infrav1beta1.AzureCluster) - return Convert_v1alpha4_AzureCluster_To_v1beta1_AzureCluster(src, dst, nil) + if err := Convert_v1alpha4_AzureCluster_To_v1beta1_AzureCluster(src, dst, nil); err != nil { + return err + } + + // Manually restore data. + restored := &infrav1beta1.AzureCluster{} + if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok { + return err + } + + // Restore list of virtual network peerings + dst.Spec.NetworkSpec.Vnet.Peerings = restored.Spec.NetworkSpec.Vnet.Peerings + + return nil } // ConvertFrom converts from the Hub version (v1beta1) to this version. func (dst *AzureCluster) ConvertFrom(srcRaw conversion.Hub) error { // nolint src := srcRaw.(*infrav1beta1.AzureCluster) - return Convert_v1beta1_AzureCluster_To_v1alpha4_AzureCluster(src, dst, nil) + if err := Convert_v1beta1_AzureCluster_To_v1alpha4_AzureCluster(src, dst, nil); err != nil { + return err + } + + // Preserve Hub data on down-conversion. + if err := utilconversion.MarshalData(src, dst); err != nil { + return err + } + + return nil } // ConvertTo converts this AzureClusterList to the Hub version (v1beta1). @@ -44,3 +69,13 @@ func (dst *AzureClusterList) ConvertFrom(srcRaw conversion.Hub) error { // nolin src := srcRaw.(*infrav1beta1.AzureClusterList) return Convert_v1beta1_AzureClusterList_To_v1alpha4_AzureClusterList(src, dst, nil) } + +// Convert_v1beta1_VnetSpec_To_v1alpha4_VnetSpec. +func Convert_v1beta1_VnetSpec_To_v1alpha4_VnetSpec(in *infrav1beta1.VnetSpec, out *VnetSpec, s apiconversion.Scope) error { //nolint + return autoConvert_v1beta1_VnetSpec_To_v1alpha4_VnetSpec(in, out, s) +} + +// Convert_v1alpha4_VnetSpec_To_v1beta1_VnetSpec is an autogenerated conversion function. +func Convert_v1alpha4_VnetSpec_To_v1beta1_VnetSpec(in *VnetSpec, out *infrav1beta1.VnetSpec, s apiconversion.Scope) error { + return autoConvert_v1alpha4_VnetSpec_To_v1beta1_VnetSpec(in, out, s) +} diff --git a/api/v1alpha4/zz_generated.conversion.go b/api/v1alpha4/zz_generated.conversion.go index a7b54d38aa6..eaa71c8df49 100644 --- a/api/v1alpha4/zz_generated.conversion.go +++ b/api/v1alpha4/zz_generated.conversion.go @@ -511,12 +511,12 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*VnetSpec)(nil), (*v1beta1.VnetSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + if err := s.AddConversionFunc((*VnetSpec)(nil), (*v1beta1.VnetSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha4_VnetSpec_To_v1beta1_VnetSpec(a.(*VnetSpec), b.(*v1beta1.VnetSpec), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta1.VnetSpec)(nil), (*VnetSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + if err := s.AddConversionFunc((*v1beta1.VnetSpec)(nil), (*VnetSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_VnetSpec_To_v1alpha4_VnetSpec(a.(*v1beta1.VnetSpec), b.(*VnetSpec), scope) }); err != nil { return err @@ -1995,21 +1995,12 @@ func autoConvert_v1alpha4_VnetSpec_To_v1beta1_VnetSpec(in *VnetSpec, out *v1beta return nil } -// Convert_v1alpha4_VnetSpec_To_v1beta1_VnetSpec is an autogenerated conversion function. -func Convert_v1alpha4_VnetSpec_To_v1beta1_VnetSpec(in *VnetSpec, out *v1beta1.VnetSpec, s conversion.Scope) error { - return autoConvert_v1alpha4_VnetSpec_To_v1beta1_VnetSpec(in, out, s) -} - func autoConvert_v1beta1_VnetSpec_To_v1alpha4_VnetSpec(in *v1beta1.VnetSpec, out *VnetSpec, s conversion.Scope) error { out.ResourceGroup = in.ResourceGroup out.ID = in.ID out.Name = in.Name out.CIDRBlocks = *(*[]string)(unsafe.Pointer(&in.CIDRBlocks)) + // WARNING: in.Peerings requires manual conversion: does not exist in peer-type out.Tags = *(*Tags)(unsafe.Pointer(&in.Tags)) return nil } - -// Convert_v1beta1_VnetSpec_To_v1alpha4_VnetSpec is an autogenerated conversion function. -func Convert_v1beta1_VnetSpec_To_v1alpha4_VnetSpec(in *v1beta1.VnetSpec, out *VnetSpec, s conversion.Scope) error { - return autoConvert_v1beta1_VnetSpec_To_v1alpha4_VnetSpec(in, out, s) -} diff --git a/api/v1beta1/azurecluster_default.go b/api/v1beta1/azurecluster_default.go index 08810792a6c..f4374e3329d 100644 --- a/api/v1beta1/azurecluster_default.go +++ b/api/v1beta1/azurecluster_default.go @@ -53,6 +53,7 @@ func (c *AzureCluster) setNetworkSpecDefaults() { c.setVnetDefaults() c.setBastionDefaults() c.setSubnetDefaults() + c.setVnetPeeringDefaults() c.setAPIServerLBDefaults() c.setNodeOutboundLBDefaults() c.setControlPlaneOutboundLBDefaults() @@ -147,6 +148,14 @@ func (c *AzureCluster) setSubnetDefaults() { } } +func (c *AzureCluster) setVnetPeeringDefaults() { + for i, peering := range c.Spec.NetworkSpec.Vnet.Peerings { + if peering.ResourceGroup == "" { + c.Spec.NetworkSpec.Vnet.Peerings[i].ResourceGroup = c.Spec.ResourceGroup + } + } +} + func setSecurityRuleDefaults(sg *SecurityGroup) { for i := range sg.SecurityRules { if sg.SecurityRules[i].Direction == "" { diff --git a/api/v1beta1/azurecluster_default_test.go b/api/v1beta1/azurecluster_default_test.go index 4e82fc44a09..4e5c03749e1 100644 --- a/api/v1beta1/azurecluster_default_test.go +++ b/api/v1beta1/azurecluster_default_test.go @@ -621,6 +621,124 @@ func TestSubnetDefaults(t *testing.T) { } } +func TestVnetPeeringDefaults(t *testing.T) { + cases := []struct { + name string + cluster *AzureCluster + output *AzureCluster + }{ + { + name: "no peering", + cluster: &AzureCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster-test", + }, + Spec: AzureClusterSpec{ + NetworkSpec: NetworkSpec{}, + }, + }, + output: &AzureCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster-test", + }, + Spec: AzureClusterSpec{ + NetworkSpec: NetworkSpec{}, + }, + }, + }, + { + name: "peering with resource group", + cluster: &AzureCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster-test", + }, + Spec: AzureClusterSpec{ + ResourceGroup: "cluster-test", + NetworkSpec: NetworkSpec{ + Vnet: VnetSpec{ + Peerings: VnetPeerings{ + { + RemoteVnetName: "my-vnet", + ResourceGroup: "cluster-test", + }, + }, + }, + }, + }, + }, + output: &AzureCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster-test", + }, + Spec: AzureClusterSpec{ + ResourceGroup: "cluster-test", + NetworkSpec: NetworkSpec{ + Vnet: VnetSpec{ + Peerings: VnetPeerings{ + { + RemoteVnetName: "my-vnet", + ResourceGroup: "cluster-test", + }, + }, + }, + }, + }, + }, + }, + { + name: "peering without resource group", + cluster: &AzureCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster-test", + }, + Spec: AzureClusterSpec{ + ResourceGroup: "cluster-test", + NetworkSpec: NetworkSpec{ + Vnet: VnetSpec{ + Peerings: VnetPeerings{ + { + RemoteVnetName: "my-vnet", + }, + }, + }, + }, + }, + }, + output: &AzureCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster-test", + }, + Spec: AzureClusterSpec{ + ResourceGroup: "cluster-test", + NetworkSpec: NetworkSpec{ + Vnet: VnetSpec{ + Peerings: VnetPeerings{ + { + RemoteVnetName: "my-vnet", + ResourceGroup: "cluster-test", + }, + }, + }, + }, + }, + }, + }, + } + + for _, c := range cases { + tc := c + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + tc.cluster.setVnetPeeringDefaults() + if !reflect.DeepEqual(tc.cluster, tc.output) { + expected, _ := json.MarshalIndent(tc.output, "", "\t") + actual, _ := json.MarshalIndent(tc.cluster, "", "\t") + t.Errorf("Expected %s, got %s", string(expected), string(actual)) + } + }) + } +} + func TestAPIServerLBDefaults(t *testing.T) { cases := []struct { name string diff --git a/api/v1beta1/azurecluster_validation.go b/api/v1beta1/azurecluster_validation.go index 4b1231974b8..063c405f683 100644 --- a/api/v1beta1/azurecluster_validation.go +++ b/api/v1beta1/azurecluster_validation.go @@ -120,6 +120,8 @@ func validateNetworkSpec(networkSpec NetworkSpec, old NetworkSpec, fldPath *fiel allErrs = append(allErrs, validateVnetCIDR(networkSpec.Vnet.CIDRBlocks, fldPath.Child("cidrBlocks"))...) allErrs = append(allErrs, validateSubnets(networkSpec.Subnets, networkSpec.Vnet, fldPath.Child("subnets"))...) + + allErrs = append(allErrs, validateVnetPeerings(networkSpec.Vnet.Peerings, fldPath.Child("peerings"))...) } var cidrBlocks []string @@ -256,6 +258,21 @@ func validateVnetCIDR(vnetCIDRBlocks []string, fldPath *field.Path) field.ErrorL return allErrs } +// validateVnetPeerings validates a list of virtual network peerings. +func validateVnetPeerings(peerings VnetPeerings, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + vnetIdentifiers := make(map[string]bool, len(peerings)) + + for _, peering := range peerings { + vnetIdentifier := peering.ResourceGroup + "/" + peering.RemoteVnetName + if _, ok := vnetIdentifiers[vnetIdentifier]; ok { + allErrs = append(allErrs, field.Duplicate(fldPath, vnetIdentifier)) + } + vnetIdentifiers[vnetIdentifier] = true + } + return allErrs +} + // validateLoadBalancerName validates the Name of a Load Balancer. func validateLoadBalancerName(name string, fldPath *field.Path) *field.Error { if success, _ := regexp.Match(loadBalancerRegex, []byte(name)); !success { diff --git a/api/v1beta1/types.go b/api/v1beta1/types.go index ca9ce75560b..85637aa49fe 100644 --- a/api/v1beta1/types.go +++ b/api/v1beta1/types.go @@ -106,11 +106,28 @@ type VnetSpec struct { // +optional CIDRBlocks []string `json:"cidrBlocks,omitempty"` + // Peerings defines a list of peerings of the newly created virtual network with existing virtual networks. + // +optional + Peerings VnetPeerings `json:"peerings,omitempty"` + // Tags is a collection of tags describing the resource. // +optional Tags Tags `json:"tags,omitempty"` } +// VnetPeeringSpec specifies an existing remote virtual network to peer with the AzureCluster's virtual network. +type VnetPeeringSpec struct { + // ResourceGroup is the resource group name of the remote virtual network. + // +optional + ResourceGroup string `json:"resourceGroup,omitempty"` + + // RemoteVnetName defines name of the remote virtual network. + RemoteVnetName string `json:"remoteVnetName"` +} + +// VnetPeerings is a slice of VnetPeering. +type VnetPeerings []VnetPeeringSpec + // IsManaged returns true if the vnet is managed. func (v *VnetSpec) IsManaged(clusterName string) bool { return v.ID == "" || v.Tags.HasOwned(clusterName) diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 854924c2b36..440e3f4590b 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -1297,6 +1297,40 @@ func (in *VM) DeepCopy() *VM { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VnetPeeringSpec) DeepCopyInto(out *VnetPeeringSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VnetPeeringSpec. +func (in *VnetPeeringSpec) DeepCopy() *VnetPeeringSpec { + if in == nil { + return nil + } + out := new(VnetPeeringSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in VnetPeerings) DeepCopyInto(out *VnetPeerings) { + { + in := &in + *out = make(VnetPeerings, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VnetPeerings. +func (in VnetPeerings) DeepCopy() VnetPeerings { + if in == nil { + return nil + } + out := new(VnetPeerings) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VnetSpec) DeepCopyInto(out *VnetSpec) { *out = *in @@ -1305,6 +1339,11 @@ func (in *VnetSpec) DeepCopyInto(out *VnetSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Peerings != nil { + in, out := &in.Peerings, &out.Peerings + *out = make(VnetPeerings, len(*in)) + copy(*out, *in) + } if in.Tags != nil { in, out := &in.Tags, &out.Tags *out = make(Tags, len(*in)) diff --git a/azure/defaults.go b/azure/defaults.go index 78d0b356e92..2f04ecc6b9e 100644 --- a/azure/defaults.go +++ b/azure/defaults.go @@ -169,6 +169,11 @@ func GenerateDataDiskName(machineName, nameSuffix string) string { return fmt.Sprintf("%s_%s", machineName, nameSuffix) } +// GenerateVnetPeeringName generates the name for a peering between two vnets. +func GenerateVnetPeeringName(sourceVnetName string, remoteVnetName string) string { + return fmt.Sprintf("%s-To-%s", sourceVnetName, remoteVnetName) +} + // GenerateAvailabilitySetName generates the name of a availability set based on the cluster name and the node group. // node group identifies the set of nodes that belong to this availability set: // For control plane nodes, this will be `control-plane`. diff --git a/azure/scope/cluster.go b/azure/scope/cluster.go index f7433c1fc43..d13c12ce167 100644 --- a/azure/scope/cluster.go +++ b/azure/scope/cluster.go @@ -297,6 +297,32 @@ func (s *ClusterScope) GroupSpec() azure.ResourceSpecGetter { } } +// VnetPeeringSpecs returns the virtual network peering specs. +func (s *ClusterScope) VnetPeeringSpecs() []azure.VnetPeeringSpec { + peeringSpecs := make([]azure.VnetPeeringSpec, 2*len(s.Vnet().Peerings)) + + for i, peering := range s.Vnet().Peerings { + forwardPeering := azure.VnetPeeringSpec{ + PeeringName: azure.GenerateVnetPeeringName(s.Vnet().Name, peering.RemoteVnetName), + SourceVnetName: s.Vnet().Name, + SourceResourceGroup: s.Vnet().ResourceGroup, + RemoteVnetName: peering.RemoteVnetName, + RemoteResourceGroup: peering.ResourceGroup, + } + reversePeering := azure.VnetPeeringSpec{ + PeeringName: azure.GenerateVnetPeeringName(peering.RemoteVnetName, s.Vnet().Name), + SourceVnetName: peering.RemoteVnetName, + SourceResourceGroup: peering.ResourceGroup, + RemoteVnetName: s.Vnet().Name, + RemoteResourceGroup: s.Vnet().ResourceGroup, + } + peeringSpecs[i*2] = forwardPeering + peeringSpecs[i*2+1] = reversePeering + } + + return peeringSpecs +} + // VNetSpec returns the virtual network spec. func (s *ClusterScope) VNetSpec() azure.VNetSpec { return azure.VNetSpec{ @@ -308,13 +334,24 @@ func (s *ClusterScope) VNetSpec() azure.VNetSpec { // PrivateDNSSpec returns the private dns zone spec. func (s *ClusterScope) PrivateDNSSpec() *azure.PrivateDNSSpec { - var spec *azure.PrivateDNSSpec + var specs *azure.PrivateDNSSpec if s.IsAPIServerPrivate() { - spec = &azure.PrivateDNSSpec{ - ZoneName: s.GetPrivateDNSZoneName(), + links := make([]azure.PrivateDNSLinkSpec, 1+len(s.Vnet().Peerings)) + links[0] = azure.PrivateDNSLinkSpec{ VNetName: s.Vnet().Name, VNetResourceGroup: s.Vnet().ResourceGroup, LinkName: azure.GenerateVNetLinkName(s.Vnet().Name), + } + for i, peering := range s.Vnet().Peerings { + links[i+1] = azure.PrivateDNSLinkSpec{ + VNetName: peering.RemoteVnetName, + VNetResourceGroup: peering.ResourceGroup, + LinkName: azure.GenerateVNetLinkName(peering.RemoteVnetName), + } + } + specs = &azure.PrivateDNSSpec{ + ZoneName: s.GetPrivateDNSZoneName(), + Links: links, Records: []infrav1.AddressRecord{ { Hostname: azure.PrivateAPIServerHostname, @@ -323,7 +360,8 @@ func (s *ClusterScope) PrivateDNSSpec() *azure.PrivateDNSSpec { }, } } - return spec + + return specs } // BastionSpec returns the bastion spec. diff --git a/azure/services/privatedns/privatedns.go b/azure/services/privatedns/privatedns.go index 6291d300589..f4aced09871 100644 --- a/azure/services/privatedns/privatedns.go +++ b/azure/services/privatedns/privatedns.go @@ -65,22 +65,24 @@ func (s *Service) Reconcile(ctx context.Context) error { } s.Scope.V(2).Info("successfully created private DNS zone", "private dns zone", zoneSpec.ZoneName) - // Link the virtual network. - s.Scope.V(2).Info("creating a virtual network link", "virtual network", zoneSpec.VNetName, "private dns zone", zoneSpec.ZoneName) - link := privatedns.VirtualNetworkLink{ - VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ - VirtualNetwork: &privatedns.SubResource{ - ID: to.StringPtr(azure.VNetID(s.Scope.SubscriptionID(), zoneSpec.VNetResourceGroup, zoneSpec.VNetName)), + for _, linkSpec := range zoneSpec.Links { + // Link each virtual network. + s.Scope.V(2).Info("creating a virtual network link", "virtual network", linkSpec.VNetName, "private dns zone", zoneSpec.ZoneName) + link := privatedns.VirtualNetworkLink{ + VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ + VirtualNetwork: &privatedns.SubResource{ + ID: to.StringPtr(azure.VNetID(s.Scope.SubscriptionID(), linkSpec.VNetResourceGroup, linkSpec.VNetName)), + }, + RegistrationEnabled: to.BoolPtr(false), }, - RegistrationEnabled: to.BoolPtr(false), - }, - Location: to.StringPtr(azure.Global), - } - err = s.client.CreateOrUpdateLink(ctx, s.Scope.ResourceGroup(), zoneSpec.ZoneName, zoneSpec.LinkName, link) - if err != nil { - return errors.Wrapf(err, "failed to create virtual network link %s", zoneSpec.LinkName) + Location: to.StringPtr(azure.Global), + } + err = s.client.CreateOrUpdateLink(ctx, s.Scope.ResourceGroup(), zoneSpec.ZoneName, linkSpec.LinkName, link) + if err != nil { + return errors.Wrapf(err, "failed to create virtual network link %s", linkSpec.LinkName) + } + s.Scope.V(2).Info("successfully created virtual network link", "virtual network", linkSpec.VNetName, "private dns zone", zoneSpec.ZoneName) } - s.Scope.V(2).Info("successfully created virtual network link", "virtual network", zoneSpec.VNetName, "private dns zone", zoneSpec.ZoneName) // Create the record(s). for _, record := range zoneSpec.Records { @@ -117,16 +119,18 @@ func (s *Service) Delete(ctx context.Context) error { zoneSpec := s.Scope.PrivateDNSSpec() if zoneSpec != nil { - // Remove the virtual network link. - s.Scope.V(2).Info("removing virtual network link", "virtual network", zoneSpec.VNetName, "private dns zone", zoneSpec.ZoneName) - err := s.client.DeleteLink(ctx, s.Scope.ResourceGroup(), zoneSpec.ZoneName, zoneSpec.LinkName) - if err != nil && !azure.ResourceNotFound(err) { - return errors.Wrapf(err, "failed to delete virtual network link %s with zone %s in resource group %s", zoneSpec.VNetName, zoneSpec.ZoneName, s.Scope.ResourceGroup()) + for _, linkSpec := range zoneSpec.Links { + // Remove each virtual network link. + s.Scope.V(2).Info("removing virtual network link", "virtual network", linkSpec.VNetName, "private dns zone", zoneSpec.ZoneName) + err := s.client.DeleteLink(ctx, s.Scope.ResourceGroup(), zoneSpec.ZoneName, linkSpec.LinkName) + if err != nil && !azure.ResourceNotFound(err) { + return errors.Wrapf(err, "failed to delete virtual network link %s with zone %s in resource group %s", linkSpec.VNetName, zoneSpec.ZoneName, s.Scope.ResourceGroup()) + } } // Delete the private DNS zone, which also deletes all records. s.Scope.V(2).Info("deleting private dns zone", "private dns zone", zoneSpec.ZoneName) - err = s.client.DeleteZone(ctx, s.Scope.ResourceGroup(), zoneSpec.ZoneName) + err := s.client.DeleteZone(ctx, s.Scope.ResourceGroup(), zoneSpec.ZoneName) if err != nil && azure.ResourceNotFound(err) { // already deleted return nil diff --git a/azure/services/privatedns/privatedns_test.go b/azure/services/privatedns/privatedns_test.go index 1b21a5ca63f..c318bb9497d 100644 --- a/azure/services/privatedns/privatedns_test.go +++ b/azure/services/privatedns/privatedns_test.go @@ -54,10 +54,14 @@ func TestReconcilePrivateDNS(t *testing.T) { expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ - ZoneName: "my-dns-zone", - VNetName: "my-vnet", - VNetResourceGroup: "vnet-rg", - LinkName: "my-link", + ZoneName: "my-dns-zone", + Links: []azure.PrivateDNSLinkSpec{ + { + VNetName: "my-vnet", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link", + }, + }, Records: []infrav1.AddressRecord{ { Hostname: "hostname-1", @@ -89,16 +93,79 @@ func TestReconcilePrivateDNS(t *testing.T) { }) }, }, + { + name: "create multiple ipv4 private dns successfully", + expectedError: "", + expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ + ZoneName: "my-dns-zone", + Links: []azure.PrivateDNSLinkSpec{ + { + VNetName: "my-vnet-1", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-1", + }, + { + VNetName: "my-vnet-2", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-2", + }, + }, + Records: []infrav1.AddressRecord{ + { + Hostname: "hostname-1", + IP: "10.0.0.8", + }, + }, + }) + s.ResourceGroup().AnyTimes().Return("my-rg") + s.SubscriptionID().AnyTimes().Return("123") + m.CreateOrUpdateZone(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.PrivateZone{Location: to.StringPtr(azure.Global)}) + m.CreateOrUpdateLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1", privatedns.VirtualNetworkLink{ + VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ + VirtualNetwork: &privatedns.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/vnet-rg/providers/Microsoft.Network/virtualNetworks/my-vnet-1"), + }, + RegistrationEnabled: to.BoolPtr(false), + }, + Location: to.StringPtr(azure.Global), + }) + m.CreateOrUpdateLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2", privatedns.VirtualNetworkLink{ + VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ + VirtualNetwork: &privatedns.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/vnet-rg/providers/Microsoft.Network/virtualNetworks/my-vnet-2"), + }, + RegistrationEnabled: to.BoolPtr(false), + }, + Location: to.StringPtr(azure.Global), + }) + m.CreateOrUpdateRecordSet(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.A, "hostname-1", privatedns.RecordSet{ + RecordSetProperties: &privatedns.RecordSetProperties{ + TTL: to.Int64Ptr(300), + ARecords: &[]privatedns.ARecord{ + { + Ipv4Address: to.StringPtr("10.0.0.8"), + }, + }, + }, + }) + }, + }, { name: "create ipv6 private dns successfully", expectedError: "", expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ - ZoneName: "my-dns-zone", - VNetName: "my-vnet", - VNetResourceGroup: "vnet-rg", - LinkName: "my-link", + ZoneName: "my-dns-zone", + Links: []azure.PrivateDNSLinkSpec{ + { + VNetName: "my-vnet", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link", + }, + }, Records: []infrav1.AddressRecord{ { Hostname: "hostname-2", @@ -130,16 +197,79 @@ func TestReconcilePrivateDNS(t *testing.T) { }) }, }, + { + name: "create multiple ipv6 private dns successfully", + expectedError: "", + expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ + ZoneName: "my-dns-zone", + Links: []azure.PrivateDNSLinkSpec{ + { + VNetName: "my-vnet-1", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-1", + }, + { + VNetName: "my-vnet-2", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-2", + }, + }, + Records: []infrav1.AddressRecord{ + { + Hostname: "hostname-2", + IP: "2603:1030:805:2::b", + }, + }, + }) + s.ResourceGroup().AnyTimes().Return("my-rg") + s.SubscriptionID().AnyTimes().Return("123") + m.CreateOrUpdateZone(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.PrivateZone{Location: to.StringPtr(azure.Global)}) + m.CreateOrUpdateLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1", privatedns.VirtualNetworkLink{ + VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ + VirtualNetwork: &privatedns.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/vnet-rg/providers/Microsoft.Network/virtualNetworks/my-vnet-1"), + }, + RegistrationEnabled: to.BoolPtr(false), + }, + Location: to.StringPtr(azure.Global), + }) + m.CreateOrUpdateLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2", privatedns.VirtualNetworkLink{ + VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ + VirtualNetwork: &privatedns.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/vnet-rg/providers/Microsoft.Network/virtualNetworks/my-vnet-2"), + }, + RegistrationEnabled: to.BoolPtr(false), + }, + Location: to.StringPtr(azure.Global), + }) + m.CreateOrUpdateRecordSet(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.AAAA, "hostname-2", privatedns.RecordSet{ + RecordSetProperties: &privatedns.RecordSetProperties{ + TTL: to.Int64Ptr(300), + AaaaRecords: &[]privatedns.AaaaRecord{ + { + Ipv6Address: to.StringPtr("2603:1030:805:2::b"), + }, + }, + }, + }) + }, + }, { name: "link creation fails", expectedError: "failed to create virtual network link my-link: #: Internal Server Error: StatusCode=500", expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ - ZoneName: "my-dns-zone", - VNetName: "my-vnet", - VNetResourceGroup: "vnet-rg", - LinkName: "my-link", + ZoneName: "my-dns-zone", + Links: []azure.PrivateDNSLinkSpec{ + { + VNetName: "my-vnet", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link", + }, + }, Records: []infrav1.AddressRecord{ { Hostname: "hostname-1", @@ -161,6 +291,60 @@ func TestReconcilePrivateDNS(t *testing.T) { }).Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) }, }, + { + name: "creating multiple links fails", + expectedError: "failed to create virtual network link my-link-2: #: Internal Server Error: StatusCode=500", + expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ + ZoneName: "my-dns-zone", + Links: []azure.PrivateDNSLinkSpec{ + { + VNetName: "my-vnet-1", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-1", + }, + { + VNetName: "my-vnet-2", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-2", + }, + { + VNetName: "my-vnet-3", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-3", + }, + }, + Records: []infrav1.AddressRecord{ + { + Hostname: "hostname-1", + IP: "10.0.0.8", + }, + }, + }) + s.ResourceGroup().AnyTimes().Return("my-rg") + s.SubscriptionID().AnyTimes().Return("123") + m.CreateOrUpdateZone(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.PrivateZone{Location: to.StringPtr(azure.Global)}) + m.CreateOrUpdateLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1", privatedns.VirtualNetworkLink{ + VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ + VirtualNetwork: &privatedns.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/vnet-rg/providers/Microsoft.Network/virtualNetworks/my-vnet-1"), + }, + RegistrationEnabled: to.BoolPtr(false), + }, + Location: to.StringPtr(azure.Global), + }) + m.CreateOrUpdateLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2", privatedns.VirtualNetworkLink{ + VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ + VirtualNetwork: &privatedns.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/vnet-rg/providers/Microsoft.Network/virtualNetworks/my-vnet-2"), + }, + RegistrationEnabled: to.BoolPtr(false), + }, + Location: to.StringPtr(azure.Global), + }).Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) + }, + }, } for _, tc := range testcases { @@ -212,10 +396,14 @@ func TestDeletePrivateDNS(t *testing.T) { expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ - ZoneName: "my-dns-zone", - VNetName: "my-vnet", - VNetResourceGroup: "vnet-rg", - LinkName: "my-link", + ZoneName: "my-dns-zone", + Links: []azure.PrivateDNSLinkSpec{ + { + VNetName: "my-vnet", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link", + }, + }, Records: []infrav1.AddressRecord{ { Hostname: "hostname-1", @@ -228,16 +416,58 @@ func TestDeletePrivateDNS(t *testing.T) { m.DeleteZone(gomockinternal.AContext(), "my-rg", "my-dns-zone") }, }, + { + name: "delete the dns zone with multiple links", + expectedError: "", + expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ + ZoneName: "my-dns-zone", + Links: []azure.PrivateDNSLinkSpec{ + { + VNetName: "my-vnet-1", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-1", + }, + { + VNetName: "my-vnet-2", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-2", + }, + { + VNetName: "my-vnet-3", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-3", + }, + }, + Records: []infrav1.AddressRecord{ + { + Hostname: "hostname-1", + IP: "10.0.0.8", + }, + }, + }) + s.ResourceGroup().AnyTimes().Return("my-rg") + m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1") + m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2") + m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-3") + m.DeleteZone(gomockinternal.AContext(), "my-rg", "my-dns-zone") + }, + }, { name: "link already deleted", expectedError: "", expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ - ZoneName: "my-dns-zone", - VNetName: "my-vnet", - VNetResourceGroup: "vnet-rg", - LinkName: "my-link", + ZoneName: "my-dns-zone", + Links: []azure.PrivateDNSLinkSpec{ + { + VNetName: "my-vnet", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link", + }, + }, Records: []infrav1.AddressRecord{ { Hostname: "hostname-1", @@ -251,16 +481,59 @@ func TestDeletePrivateDNS(t *testing.T) { m.DeleteZone(gomockinternal.AContext(), "my-rg", "my-dns-zone") }, }, + { + name: "one link already deleted with multiple links", + expectedError: "", + expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ + ZoneName: "my-dns-zone", + Links: []azure.PrivateDNSLinkSpec{ + { + VNetName: "my-vnet-1", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-1", + }, + { + VNetName: "my-vnet-2", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-2", + }, + { + VNetName: "my-vnet-3", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-3", + }, + }, + Records: []infrav1.AddressRecord{ + { + Hostname: "hostname-1", + IP: "10.0.0.8", + }, + }, + }) + s.ResourceGroup().AnyTimes().Return("my-rg") + m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1") + m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2"). + Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) + m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-3") + m.DeleteZone(gomockinternal.AContext(), "my-rg", "my-dns-zone") + }, + }, { name: "zone already deleted", expectedError: "", expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ - ZoneName: "my-dns-zone", - VNetName: "my-vnet", - VNetResourceGroup: "vnet-rg", - LinkName: "my-link", + ZoneName: "my-dns-zone", + Links: []azure.PrivateDNSLinkSpec{ + { + VNetName: "my-vnet", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link", + }, + }, Records: []infrav1.AddressRecord{ { Hostname: "hostname-1", @@ -281,10 +554,14 @@ func TestDeletePrivateDNS(t *testing.T) { expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ - ZoneName: "my-dns-zone", - VNetName: "my-vnet", - VNetResourceGroup: "vnet-rg", - LinkName: "my-link", + ZoneName: "my-dns-zone", + Links: []azure.PrivateDNSLinkSpec{ + { + VNetName: "my-vnet", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link", + }, + }, Records: []infrav1.AddressRecord{ { Hostname: "hostname-1", @@ -298,15 +575,56 @@ func TestDeletePrivateDNS(t *testing.T) { }, }, { - name: "error while trying to delete the zone", + name: "error while trying to delete one link with multiple links", + expectedError: "failed to delete virtual network link my-vnet-2 with zone my-dns-zone in resource group my-rg: #: Internal Server Error: StatusCode=500", + expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ + ZoneName: "my-dns-zone", + Links: []azure.PrivateDNSLinkSpec{ + { + VNetName: "my-vnet-1", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-1", + }, + { + VNetName: "my-vnet-2", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-2", + }, + { + VNetName: "my-vnet-3", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-3", + }, + }, + Records: []infrav1.AddressRecord{ + { + Hostname: "hostname-1", + IP: "10.0.0.8", + }, + }, + }) + s.ResourceGroup().AnyTimes().Return("my-rg") + m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1") + m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2"). + Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) + }, + }, + { + name: "error while trying to delete the zone with one link", expectedError: "failed to delete private dns zone my-dns-zone in resource group my-rg: #: Internal Server Error: StatusCode=500", expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ - ZoneName: "my-dns-zone", - VNetName: "my-vnet", - VNetResourceGroup: "vnet-rg", - LinkName: "my-link", + ZoneName: "my-dns-zone", + Links: []azure.PrivateDNSLinkSpec{ + { + VNetName: "my-vnet", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link", + }, + }, Records: []infrav1.AddressRecord{ { Hostname: "hostname-1", @@ -320,6 +638,45 @@ func TestDeletePrivateDNS(t *testing.T) { Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) }, }, + { + name: "error while trying to delete the zone with multiple links", + expectedError: "failed to delete private dns zone my-dns-zone in resource group my-rg: #: Internal Server Error: StatusCode=500", + expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ + ZoneName: "my-dns-zone", + Links: []azure.PrivateDNSLinkSpec{ + { + VNetName: "my-vnet-1", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-1", + }, + { + VNetName: "my-vnet-2", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-2", + }, + { + VNetName: "my-vnet-3", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link-3", + }, + }, + Records: []infrav1.AddressRecord{ + { + Hostname: "hostname-1", + IP: "10.0.0.8", + }, + }, + }) + s.ResourceGroup().AnyTimes().Return("my-rg") + m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1") + m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2") + m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-3") + m.DeleteZone(gomockinternal.AContext(), "my-rg", "my-dns-zone"). + Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) + }, + }, } for _, tc := range testcases { diff --git a/azure/services/virtualnetworks/virtualnetworks.go b/azure/services/virtualnetworks/virtualnetworks.go index d57bf8f644b..7150ff4c69c 100644 --- a/azure/services/virtualnetworks/virtualnetworks.go +++ b/azure/services/virtualnetworks/virtualnetworks.go @@ -18,6 +18,7 @@ package virtualnetworks import ( "context" + "fmt" "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-02-01/network" "github.com/Azure/go-autorest/autorest/to" @@ -97,7 +98,9 @@ func (s *Service) Reconcile(ctx context.Context) error { }, }, } + err = s.Client.CreateOrUpdate(ctx, vnetSpec.ResourceGroup, vnetSpec.Name, vnetProperties) + if err != nil { return errors.Wrapf(err, "failed to create virtual network %s", vnetSpec.Name) } @@ -147,6 +150,7 @@ func (s *Service) getExisting(ctx context.Context, spec azure.VNetSpec) (*infrav vnet, err := s.Client.Get(ctx, spec.ResourceGroup, spec.Name) if err != nil { if azure.ResourceNotFound(err) { + s.Scope.V(2).Info(fmt.Sprintf("Resource not found for VNet %q from resource group %q", spec.Name, spec.ResourceGroup)) return nil, err } return nil, errors.Wrapf(err, "failed to get VNet %s", spec.Name) @@ -155,11 +159,13 @@ func (s *Service) getExisting(ctx context.Context, spec azure.VNetSpec) (*infrav if vnet.VirtualNetworkPropertiesFormat != nil && vnet.VirtualNetworkPropertiesFormat.AddressSpace != nil { prefixes = to.StringSlice(vnet.VirtualNetworkPropertiesFormat.AddressSpace.AddressPrefixes) } + return &infrav1.VnetSpec{ ResourceGroup: spec.ResourceGroup, ID: to.String(vnet.ID), Name: to.String(vnet.Name), CIDRBlocks: prefixes, + Peerings: s.Scope.Vnet().Peerings, Tags: converters.MapToTags(vnet.Tags), }, nil } diff --git a/azure/services/vnetpeerings/client.go b/azure/services/vnetpeerings/client.go new file mode 100644 index 00000000000..45cd63c8798 --- /dev/null +++ b/azure/services/vnetpeerings/client.go @@ -0,0 +1,96 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vnetpeerings + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-02-01/network" + "github.com/Azure/go-autorest/autorest" + + "sigs.k8s.io/cluster-api-provider-azure/azure" + "sigs.k8s.io/cluster-api-provider-azure/util/tele" +) + +// Client wraps go-sdk. +type Client interface { + Get(context.Context, string, string, string) (network.VirtualNetworkPeering, error) + CreateOrUpdate(context.Context, string, string, string, network.VirtualNetworkPeering) error + Delete(context.Context, string, string, string) error +} + +// AzureClient contains the Azure go-sdk Client. +type AzureClient struct { + peerings network.VirtualNetworkPeeringsClient +} + +var _ Client = &AzureClient{} + +// NewClient creates a new virtual network peerings client from subscription ID. +func NewClient(auth azure.Authorizer) *AzureClient { + c := newPeeringsClient(auth.SubscriptionID(), auth.BaseURI(), auth.Authorizer()) + return &AzureClient{c} +} + +// newPeeringsClient creates a new virtual network peerings client from subscription ID. +func newPeeringsClient(subscriptionID string, baseURI string, authorizer autorest.Authorizer) network.VirtualNetworkPeeringsClient { + peeringsClient := network.NewVirtualNetworkPeeringsClientWithBaseURI(baseURI, subscriptionID) + azure.SetAutoRestClientDefaults(&peeringsClient.Client, authorizer) + return peeringsClient +} + +// Get gets the specified virtual network peering by the peering name, virtual network, and resource group. +func (ac *AzureClient) Get(ctx context.Context, resourceGroupName, vnetName, peeringName string) (network.VirtualNetworkPeering, error) { + ctx, span := tele.Tracer().Start(ctx, "vnetpeerings.AzureClient.Get") + defer span.End() + + return ac.peerings.Get(ctx, resourceGroupName, vnetName, peeringName) +} + +// CreateOrUpdate creates or updates a virtual network peering in the specified virtual network. +func (ac *AzureClient) CreateOrUpdate(ctx context.Context, resourceGroupName, vnetName, peeringName string, peering network.VirtualNetworkPeering) error { + ctx, span := tele.Tracer().Start(ctx, "vnetpeerings.AzureClient.CreateOrUpdate") + defer span.End() + + future, err := ac.peerings.CreateOrUpdate(ctx, resourceGroupName, vnetName, peeringName, peering, network.SyncRemoteAddressSpaceTrue) + if err != nil { + return err + } + err = future.WaitForCompletionRef(ctx, ac.peerings.Client) + if err != nil { + return err + } + _, err = future.Result(ac.peerings) + return err +} + +// Delete deletes the specified virtual network peering. +func (ac *AzureClient) Delete(ctx context.Context, resourceGroupName, vnetName, peeringName string) error { + ctx, span := tele.Tracer().Start(ctx, "vnetpeerings.AzureClient.Delete") + defer span.End() + + future, err := ac.peerings.Delete(ctx, resourceGroupName, vnetName, peeringName) + if err != nil { + return err + } + err = future.WaitForCompletionRef(ctx, ac.peerings.Client) + if err != nil { + return err + } + _, err = future.Result(ac.peerings) + return err +} diff --git a/azure/services/vnetpeerings/mock_vnetpeerings/client_mock.go b/azure/services/vnetpeerings/mock_vnetpeerings/client_mock.go new file mode 100644 index 00000000000..d9d34bfdb56 --- /dev/null +++ b/azure/services/vnetpeerings/mock_vnetpeerings/client_mock.go @@ -0,0 +1,95 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by MockGen. DO NOT EDIT. +// Source: ../client.go + +// Package mock_vnetpeerings is a generated GoMock package. +package mock_vnetpeerings + +import ( + context "context" + reflect "reflect" + + network "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-02-01/network" + gomock "github.com/golang/mock/gomock" +) + +// MockClient is a mock of Client interface. +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// CreateOrUpdate mocks base method. +func (m *MockClient) CreateOrUpdate(arg0 context.Context, arg1, arg2, arg3 string, arg4 network.VirtualNetworkPeering) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateOrUpdate", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateOrUpdate indicates an expected call of CreateOrUpdate. +func (mr *MockClientMockRecorder) CreateOrUpdate(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdate", reflect.TypeOf((*MockClient)(nil).CreateOrUpdate), arg0, arg1, arg2, arg3, arg4) +} + +// Delete mocks base method. +func (m *MockClient) Delete(arg0 context.Context, arg1, arg2, arg3 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockClientMockRecorder) Delete(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockClient)(nil).Delete), arg0, arg1, arg2, arg3) +} + +// Get mocks base method. +func (m *MockClient) Get(arg0 context.Context, arg1, arg2, arg3 string) (network.VirtualNetworkPeering, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(network.VirtualNetworkPeering) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockClientMockRecorder) Get(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClient)(nil).Get), arg0, arg1, arg2, arg3) +} diff --git a/azure/services/vnetpeerings/mock_vnetpeerings/doc.go b/azure/services/vnetpeerings/mock_vnetpeerings/doc.go new file mode 100644 index 00000000000..8bd3fe24d27 --- /dev/null +++ b/azure/services/vnetpeerings/mock_vnetpeerings/doc.go @@ -0,0 +1,22 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Run go generate to regenerate this mock. +//go:generate ../../../../hack/tools/bin/mockgen -destination client_mock.go -package mock_vnetpeerings -source ../client.go Client +//go:generate ../../../../hack/tools/bin/mockgen -destination vnetpeerings_mock.go -package mock_vnetpeerings -source ../vnetpeerings.go VnetPeeringScope +//go:generate /usr/bin/env bash -c "cat ../../../../hack/boilerplate/boilerplate.generatego.txt client_mock.go > _client_mock.go && mv _client_mock.go client_mock.go" +//go:generate /usr/bin/env bash -c "cat ../../../../hack/boilerplate/boilerplate.generatego.txt vnetpeerings_mock.go > _vnetpeerings_mock.go && mv _vnetpeerings_mock.go vnetpeerings_mock.go" +package mock_vnetpeerings //nolint diff --git a/azure/services/vnetpeerings/mock_vnetpeerings/vnetpeerings_mock.go b/azure/services/vnetpeerings/mock_vnetpeerings/vnetpeerings_mock.go new file mode 100644 index 00000000000..01a07b5b165 --- /dev/null +++ b/azure/services/vnetpeerings/mock_vnetpeerings/vnetpeerings_mock.go @@ -0,0 +1,302 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by MockGen. DO NOT EDIT. +// Source: ../vnetpeerings.go + +// Package mock_vnetpeerings is a generated GoMock package. +package mock_vnetpeerings + +import ( + reflect "reflect" + + autorest "github.com/Azure/go-autorest/autorest" + logr "github.com/go-logr/logr" + gomock "github.com/golang/mock/gomock" + v1beta1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" + azure "sigs.k8s.io/cluster-api-provider-azure/azure" +) + +// MockVnetPeeringScope is a mock of VnetPeeringScope interface. +type MockVnetPeeringScope struct { + ctrl *gomock.Controller + recorder *MockVnetPeeringScopeMockRecorder +} + +// MockVnetPeeringScopeMockRecorder is the mock recorder for MockVnetPeeringScope. +type MockVnetPeeringScopeMockRecorder struct { + mock *MockVnetPeeringScope +} + +// NewMockVnetPeeringScope creates a new mock instance. +func NewMockVnetPeeringScope(ctrl *gomock.Controller) *MockVnetPeeringScope { + mock := &MockVnetPeeringScope{ctrl: ctrl} + mock.recorder = &MockVnetPeeringScopeMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockVnetPeeringScope) EXPECT() *MockVnetPeeringScopeMockRecorder { + return m.recorder +} + +// Authorizer mocks base method. +func (m *MockVnetPeeringScope) Authorizer() autorest.Authorizer { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Authorizer") + ret0, _ := ret[0].(autorest.Authorizer) + return ret0 +} + +// Authorizer indicates an expected call of Authorizer. +func (mr *MockVnetPeeringScopeMockRecorder) Authorizer() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authorizer", reflect.TypeOf((*MockVnetPeeringScope)(nil).Authorizer)) +} + +// BaseURI mocks base method. +func (m *MockVnetPeeringScope) BaseURI() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BaseURI") + ret0, _ := ret[0].(string) + return ret0 +} + +// BaseURI indicates an expected call of BaseURI. +func (mr *MockVnetPeeringScopeMockRecorder) BaseURI() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BaseURI", reflect.TypeOf((*MockVnetPeeringScope)(nil).BaseURI)) +} + +// ClientID mocks base method. +func (m *MockVnetPeeringScope) ClientID() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ClientID") + ret0, _ := ret[0].(string) + return ret0 +} + +// ClientID indicates an expected call of ClientID. +func (mr *MockVnetPeeringScopeMockRecorder) ClientID() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientID", reflect.TypeOf((*MockVnetPeeringScope)(nil).ClientID)) +} + +// ClientSecret mocks base method. +func (m *MockVnetPeeringScope) ClientSecret() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ClientSecret") + ret0, _ := ret[0].(string) + return ret0 +} + +// ClientSecret indicates an expected call of ClientSecret. +func (mr *MockVnetPeeringScopeMockRecorder) ClientSecret() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientSecret", reflect.TypeOf((*MockVnetPeeringScope)(nil).ClientSecret)) +} + +// CloudEnvironment mocks base method. +func (m *MockVnetPeeringScope) CloudEnvironment() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CloudEnvironment") + ret0, _ := ret[0].(string) + return ret0 +} + +// CloudEnvironment indicates an expected call of CloudEnvironment. +func (mr *MockVnetPeeringScopeMockRecorder) CloudEnvironment() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloudEnvironment", reflect.TypeOf((*MockVnetPeeringScope)(nil).CloudEnvironment)) +} + +// ClusterName mocks base method. +func (m *MockVnetPeeringScope) ClusterName() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ClusterName") + ret0, _ := ret[0].(string) + return ret0 +} + +// ClusterName indicates an expected call of ClusterName. +func (mr *MockVnetPeeringScopeMockRecorder) ClusterName() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterName", reflect.TypeOf((*MockVnetPeeringScope)(nil).ClusterName)) +} + +// Enabled mocks base method. +func (m *MockVnetPeeringScope) Enabled() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Enabled") + ret0, _ := ret[0].(bool) + return ret0 +} + +// Enabled indicates an expected call of Enabled. +func (mr *MockVnetPeeringScopeMockRecorder) Enabled() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enabled", reflect.TypeOf((*MockVnetPeeringScope)(nil).Enabled)) +} + +// Error mocks base method. +func (m *MockVnetPeeringScope) Error(err error, msg string, keysAndValues ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{err, msg} + for _, a := range keysAndValues { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Error", varargs...) +} + +// Error indicates an expected call of Error. +func (mr *MockVnetPeeringScopeMockRecorder) Error(err, msg interface{}, keysAndValues ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{err, msg}, keysAndValues...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockVnetPeeringScope)(nil).Error), varargs...) +} + +// HashKey mocks base method. +func (m *MockVnetPeeringScope) HashKey() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HashKey") + ret0, _ := ret[0].(string) + return ret0 +} + +// HashKey indicates an expected call of HashKey. +func (mr *MockVnetPeeringScopeMockRecorder) HashKey() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HashKey", reflect.TypeOf((*MockVnetPeeringScope)(nil).HashKey)) +} + +// Info mocks base method. +func (m *MockVnetPeeringScope) Info(msg string, keysAndValues ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keysAndValues { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Info", varargs...) +} + +// Info indicates an expected call of Info. +func (mr *MockVnetPeeringScopeMockRecorder) Info(msg interface{}, keysAndValues ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keysAndValues...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockVnetPeeringScope)(nil).Info), varargs...) +} + +// SubscriptionID mocks base method. +func (m *MockVnetPeeringScope) SubscriptionID() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubscriptionID") + ret0, _ := ret[0].(string) + return ret0 +} + +// SubscriptionID indicates an expected call of SubscriptionID. +func (mr *MockVnetPeeringScopeMockRecorder) SubscriptionID() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscriptionID", reflect.TypeOf((*MockVnetPeeringScope)(nil).SubscriptionID)) +} + +// TenantID mocks base method. +func (m *MockVnetPeeringScope) TenantID() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TenantID") + ret0, _ := ret[0].(string) + return ret0 +} + +// TenantID indicates an expected call of TenantID. +func (mr *MockVnetPeeringScopeMockRecorder) TenantID() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TenantID", reflect.TypeOf((*MockVnetPeeringScope)(nil).TenantID)) +} + +// V mocks base method. +func (m *MockVnetPeeringScope) V(level int) logr.Logger { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "V", level) + ret0, _ := ret[0].(logr.Logger) + return ret0 +} + +// V indicates an expected call of V. +func (mr *MockVnetPeeringScopeMockRecorder) V(level interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "V", reflect.TypeOf((*MockVnetPeeringScope)(nil).V), level) +} + +// Vnet mocks base method. +func (m *MockVnetPeeringScope) Vnet() *v1beta1.VnetSpec { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Vnet") + ret0, _ := ret[0].(*v1beta1.VnetSpec) + return ret0 +} + +// Vnet indicates an expected call of Vnet. +func (mr *MockVnetPeeringScopeMockRecorder) Vnet() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Vnet", reflect.TypeOf((*MockVnetPeeringScope)(nil).Vnet)) +} + +// VnetPeeringSpecs mocks base method. +func (m *MockVnetPeeringScope) VnetPeeringSpecs() []azure.VnetPeeringSpec { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "VnetPeeringSpecs") + ret0, _ := ret[0].([]azure.VnetPeeringSpec) + return ret0 +} + +// VnetPeeringSpecs indicates an expected call of VnetPeeringSpecs. +func (mr *MockVnetPeeringScopeMockRecorder) VnetPeeringSpecs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VnetPeeringSpecs", reflect.TypeOf((*MockVnetPeeringScope)(nil).VnetPeeringSpecs)) +} + +// WithName mocks base method. +func (m *MockVnetPeeringScope) WithName(name string) logr.Logger { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithName", name) + ret0, _ := ret[0].(logr.Logger) + return ret0 +} + +// WithName indicates an expected call of WithName. +func (mr *MockVnetPeeringScopeMockRecorder) WithName(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithName", reflect.TypeOf((*MockVnetPeeringScope)(nil).WithName), name) +} + +// WithValues mocks base method. +func (m *MockVnetPeeringScope) WithValues(keysAndValues ...interface{}) logr.Logger { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range keysAndValues { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "WithValues", varargs...) + ret0, _ := ret[0].(logr.Logger) + return ret0 +} + +// WithValues indicates an expected call of WithValues. +func (mr *MockVnetPeeringScopeMockRecorder) WithValues(keysAndValues ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithValues", reflect.TypeOf((*MockVnetPeeringScope)(nil).WithValues), keysAndValues...) +} diff --git a/azure/services/vnetpeerings/vnetpeerings.go b/azure/services/vnetpeerings/vnetpeerings.go new file mode 100644 index 00000000000..3b16583dd9d --- /dev/null +++ b/azure/services/vnetpeerings/vnetpeerings.go @@ -0,0 +1,109 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vnetpeerings + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-02-01/network" + "github.com/Azure/go-autorest/autorest/to" + "github.com/go-logr/logr" + "github.com/pkg/errors" + + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" + "sigs.k8s.io/cluster-api-provider-azure/azure" + "sigs.k8s.io/cluster-api-provider-azure/util/tele" +) + +// VnetPeeringScope defines the scope interface for a subnet service. +type VnetPeeringScope interface { + logr.Logger + azure.Authorizer + Vnet() *infrav1.VnetSpec + ClusterName() string + SubscriptionID() string + VnetPeeringSpecs() []azure.VnetPeeringSpec +} + +// Service provides operations on Azure resources. +type Service struct { + Scope VnetPeeringScope + Client +} + +// New creates a new service. +func New(scope VnetPeeringScope) *Service { + return &Service{ + Scope: scope, + Client: NewClient(scope), + } +} + +// Reconcile gets/creates/updates a peering. +func (s *Service) Reconcile(ctx context.Context) error { + ctx, span := tele.Tracer().Start(ctx, "vnetpeerings.Service.Reconcile") + defer span.End() + + for _, peeringSpec := range s.Scope.VnetPeeringSpecs() { + vnetID := azure.VNetID(s.Scope.SubscriptionID(), peeringSpec.RemoteResourceGroup, peeringSpec.RemoteVnetName) + peeringProperties := network.VirtualNetworkPeeringPropertiesFormat{ + RemoteVirtualNetwork: &network.SubResource{ + ID: to.StringPtr(vnetID), + }, + } + + s.Scope.V(2).Info("creating peering", "peering", peeringSpec.PeeringName, "from", "vnet", peeringSpec.SourceVnetName, "to", "vnet", peeringSpec.RemoteVnetName) + err := s.Client.CreateOrUpdate( + ctx, + peeringSpec.SourceResourceGroup, + peeringSpec.SourceVnetName, + peeringSpec.PeeringName, + network.VirtualNetworkPeering{ + VirtualNetworkPeeringPropertiesFormat: &peeringProperties, + }, + ) + + if err != nil { + return errors.Wrapf(err, "failed to create peering %s in resource group %s", peeringSpec.PeeringName, s.Scope.Vnet().ResourceGroup) + } + + s.Scope.V(2).Info("successfully created peering", "peering", peeringSpec.PeeringName, "from", "vnet", peeringSpec.SourceVnetName, "to", "vnet", peeringSpec.RemoteVnetName) + } + + return nil +} + +// Delete deletes the peering with the provided name. +func (s *Service) Delete(ctx context.Context) error { + ctx, span := tele.Tracer().Start(ctx, "vnetpeerings.Service.Delete") + defer span.End() + for _, peeringSpec := range s.Scope.VnetPeeringSpecs() { + s.Scope.V(2).Info("deleting peering in vnets", "vnet1", peeringSpec.SourceVnetName, "and", "vnet2", peeringSpec.RemoteVnetName) + err := s.Client.Delete(ctx, peeringSpec.SourceResourceGroup, peeringSpec.SourceVnetName, peeringSpec.PeeringName) + if err != nil && azure.ResourceNotFound(err) { + // already deleted + continue + } + if err != nil { + return errors.Wrapf(err, "failed to delete peering %s in vnet %s and resource group %s", peeringSpec.PeeringName, peeringSpec.SourceVnetName, peeringSpec.SourceResourceGroup) + } + + s.Scope.V(2).Info("successfully deleted peering in vnet", "peering", peeringSpec.PeeringName, "vnet", peeringSpec.SourceVnetName) + } + + return nil +} diff --git a/azure/services/vnetpeerings/vnetpeerings_test.go b/azure/services/vnetpeerings/vnetpeerings_test.go new file mode 100644 index 00000000000..8ae0533f3c8 --- /dev/null +++ b/azure/services/vnetpeerings/vnetpeerings_test.go @@ -0,0 +1,551 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vnetpeerings + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-02-01/network" + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/to" + "github.com/golang/mock/gomock" + . "github.com/onsi/gomega" + "k8s.io/klog/v2/klogr" + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" + "sigs.k8s.io/cluster-api-provider-azure/azure" + "sigs.k8s.io/cluster-api-provider-azure/azure/services/vnetpeerings/mock_vnetpeerings" + gomockinternal "sigs.k8s.io/cluster-api-provider-azure/internal/test/matchers/gomock" +) + +func TestReconcileVnetPeerings(t *testing.T) { + testcases := []struct { + name string + expectedError string + expect func(s *mock_vnetpeerings.MockVnetPeeringScopeMockRecorder, m *mock_vnetpeerings.MockClientMockRecorder) + }{ + { + name: "create one peering", + expectedError: "", + expect: func(p *mock_vnetpeerings.MockVnetPeeringScopeMockRecorder, m *mock_vnetpeerings.MockClientMockRecorder) { + p.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + p.VnetPeeringSpecs().Return([]azure.VnetPeeringSpec{ + { + PeeringName: "vnet1-to-vnet2", + SourceVnetName: "vnet1", + SourceResourceGroup: "group1", + RemoteVnetName: "vnet2", + RemoteResourceGroup: "group2", + }, + }) + p.ClusterName().AnyTimes().Return("fake-cluster") + p.SubscriptionID().AnyTimes().Return("123") + m.CreateOrUpdate(gomockinternal.AContext(), "group1", "vnet1", "vnet1-to-vnet2", gomockinternal.DiffEq(network.VirtualNetworkPeering{ + VirtualNetworkPeeringPropertiesFormat: &network.VirtualNetworkPeeringPropertiesFormat{ + RemoteVirtualNetwork: &network.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/group2/providers/Microsoft.Network/virtualNetworks/vnet2"), + }, + }, + })) + }, + }, + { + name: "create no peerings", + expectedError: "", + expect: func(p *mock_vnetpeerings.MockVnetPeeringScopeMockRecorder, m *mock_vnetpeerings.MockClientMockRecorder) { + p.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + p.VnetPeeringSpecs().Return([]azure.VnetPeeringSpec{}) + p.ClusterName().AnyTimes().Return("fake-cluster") + p.SubscriptionID().AnyTimes().Return("123") + }, + }, + { + name: "create even number of peerings", + expectedError: "", + expect: func(p *mock_vnetpeerings.MockVnetPeeringScopeMockRecorder, m *mock_vnetpeerings.MockClientMockRecorder) { + p.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + p.VnetPeeringSpecs().Return([]azure.VnetPeeringSpec{ + { + PeeringName: "vnet1-to-vnet2", + SourceVnetName: "vnet1", + SourceResourceGroup: "group1", + RemoteVnetName: "vnet2", + RemoteResourceGroup: "group2", + }, + { + PeeringName: "vnet2-to-vnet1", + SourceVnetName: "vnet2", + SourceResourceGroup: "group2", + RemoteVnetName: "vnet1", + RemoteResourceGroup: "group1", + }, + }) + p.ClusterName().AnyTimes().Return("fake-cluster") + p.SubscriptionID().AnyTimes().Return("123") + m.CreateOrUpdate(gomockinternal.AContext(), "group1", "vnet1", "vnet1-to-vnet2", gomockinternal.DiffEq(network.VirtualNetworkPeering{ + VirtualNetworkPeeringPropertiesFormat: &network.VirtualNetworkPeeringPropertiesFormat{ + RemoteVirtualNetwork: &network.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/group2/providers/Microsoft.Network/virtualNetworks/vnet2"), + }, + }, + })) + m.CreateOrUpdate(gomockinternal.AContext(), "group2", "vnet2", "vnet2-to-vnet1", gomockinternal.DiffEq(network.VirtualNetworkPeering{ + VirtualNetworkPeeringPropertiesFormat: &network.VirtualNetworkPeeringPropertiesFormat{ + RemoteVirtualNetwork: &network.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/group1/providers/Microsoft.Network/virtualNetworks/vnet1"), + }, + }, + })) + }, + }, + { + name: "create odd number of peerings", + expectedError: "", + expect: func(p *mock_vnetpeerings.MockVnetPeeringScopeMockRecorder, m *mock_vnetpeerings.MockClientMockRecorder) { + p.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + p.VnetPeeringSpecs().Return([]azure.VnetPeeringSpec{ + { + PeeringName: "vnet1-to-vnet2", + SourceVnetName: "vnet1", + SourceResourceGroup: "group1", + RemoteVnetName: "vnet2", + RemoteResourceGroup: "group2", + }, + { + PeeringName: "vnet2-to-vnet1", + SourceVnetName: "vnet2", + SourceResourceGroup: "group2", + RemoteVnetName: "vnet1", + RemoteResourceGroup: "group1", + }, + { + PeeringName: "extra-peering", + SourceVnetName: "vnet3", + SourceResourceGroup: "group3", + RemoteVnetName: "vnet4", + RemoteResourceGroup: "group4", + }, + }) + p.Vnet().AnyTimes().Return(&infrav1.VnetSpec{Name: "vnet2"}) + p.ClusterName().AnyTimes().Return("fake-cluster") + p.SubscriptionID().AnyTimes().Return("123") + m.CreateOrUpdate(gomockinternal.AContext(), "group1", "vnet1", "vnet1-to-vnet2", gomockinternal.DiffEq(network.VirtualNetworkPeering{ + VirtualNetworkPeeringPropertiesFormat: &network.VirtualNetworkPeeringPropertiesFormat{ + RemoteVirtualNetwork: &network.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/group2/providers/Microsoft.Network/virtualNetworks/vnet2"), + }, + }, + })) + m.CreateOrUpdate(gomockinternal.AContext(), "group2", "vnet2", "vnet2-to-vnet1", gomockinternal.DiffEq(network.VirtualNetworkPeering{ + VirtualNetworkPeeringPropertiesFormat: &network.VirtualNetworkPeeringPropertiesFormat{ + RemoteVirtualNetwork: &network.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/group1/providers/Microsoft.Network/virtualNetworks/vnet1"), + }, + }, + })) + m.CreateOrUpdate(gomockinternal.AContext(), "group3", "vnet3", "extra-peering", gomockinternal.DiffEq(network.VirtualNetworkPeering{ + VirtualNetworkPeeringPropertiesFormat: &network.VirtualNetworkPeeringPropertiesFormat{ + RemoteVirtualNetwork: &network.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/group4/providers/Microsoft.Network/virtualNetworks/vnet4"), + }, + }, + })) + }, + }, + { + name: "create multiple peerings on one vnet", + expectedError: "", + expect: func(p *mock_vnetpeerings.MockVnetPeeringScopeMockRecorder, m *mock_vnetpeerings.MockClientMockRecorder) { + p.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + p.VnetPeeringSpecs().Return([]azure.VnetPeeringSpec{ + { + PeeringName: "vnet1-to-vnet2", + SourceVnetName: "vnet1", + SourceResourceGroup: "group1", + RemoteVnetName: "vnet2", + RemoteResourceGroup: "group2", + }, + { + PeeringName: "vnet2-to-vnet1", + SourceVnetName: "vnet2", + SourceResourceGroup: "group2", + RemoteVnetName: "vnet1", + RemoteResourceGroup: "group1", + }, + { + PeeringName: "vnet1-to-vnet3", + SourceVnetName: "vnet1", + SourceResourceGroup: "group1", + RemoteVnetName: "vnet3", + RemoteResourceGroup: "group3", + }, + { + PeeringName: "vnet3-to-vnet1", + SourceVnetName: "vnet3", + SourceResourceGroup: "group3", + RemoteVnetName: "vnet1", + RemoteResourceGroup: "group1", + }, + }) + p.ClusterName().AnyTimes().Return("fake-cluster") + p.SubscriptionID().AnyTimes().Return("123") + m.CreateOrUpdate(gomockinternal.AContext(), "group1", "vnet1", "vnet1-to-vnet2", gomockinternal.DiffEq(network.VirtualNetworkPeering{ + VirtualNetworkPeeringPropertiesFormat: &network.VirtualNetworkPeeringPropertiesFormat{ + RemoteVirtualNetwork: &network.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/group2/providers/Microsoft.Network/virtualNetworks/vnet2"), + }, + }, + })) + m.CreateOrUpdate(gomockinternal.AContext(), "group2", "vnet2", "vnet2-to-vnet1", gomockinternal.DiffEq(network.VirtualNetworkPeering{ + VirtualNetworkPeeringPropertiesFormat: &network.VirtualNetworkPeeringPropertiesFormat{ + RemoteVirtualNetwork: &network.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/group1/providers/Microsoft.Network/virtualNetworks/vnet1"), + }, + }, + })) + m.CreateOrUpdate(gomockinternal.AContext(), "group1", "vnet1", "vnet1-to-vnet3", gomockinternal.DiffEq(network.VirtualNetworkPeering{ + VirtualNetworkPeeringPropertiesFormat: &network.VirtualNetworkPeeringPropertiesFormat{ + RemoteVirtualNetwork: &network.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/group3/providers/Microsoft.Network/virtualNetworks/vnet3"), + }, + }, + })) + m.CreateOrUpdate(gomockinternal.AContext(), "group3", "vnet3", "vnet3-to-vnet1", gomockinternal.DiffEq(network.VirtualNetworkPeering{ + VirtualNetworkPeeringPropertiesFormat: &network.VirtualNetworkPeeringPropertiesFormat{ + RemoteVirtualNetwork: &network.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/group1/providers/Microsoft.Network/virtualNetworks/vnet1"), + }, + }, + })) + }, + }, + { + name: "error in creating peering where loop terminates prematurely", + expectedError: "failed to create peering vnet1-to-vnet3 in resource group group1: #: Internal Server Error: StatusCode=500", + expect: func(p *mock_vnetpeerings.MockVnetPeeringScopeMockRecorder, m *mock_vnetpeerings.MockClientMockRecorder) { + p.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + p.VnetPeeringSpecs().Return([]azure.VnetPeeringSpec{ + { + PeeringName: "vnet1-to-vnet2", + SourceVnetName: "vnet1", + SourceResourceGroup: "group1", + RemoteVnetName: "vnet2", + RemoteResourceGroup: "group2", + }, + { + PeeringName: "vnet2-to-vnet1", + SourceVnetName: "vnet2", + SourceResourceGroup: "group2", + RemoteVnetName: "vnet1", + RemoteResourceGroup: "group1", + }, + { + PeeringName: "vnet1-to-vnet3", + SourceVnetName: "vnet1", + SourceResourceGroup: "group1", + RemoteVnetName: "vnet3", + RemoteResourceGroup: "group3", + }, + { + PeeringName: "vnet3-to-vnet1", + SourceVnetName: "vnet3", + SourceResourceGroup: "group3", + RemoteVnetName: "vnet1", + RemoteResourceGroup: "group1", + }, + }) + p.Vnet().AnyTimes().Return(&infrav1.VnetSpec{ + Name: "vnet1", + ResourceGroup: "group1", + }) + p.ClusterName().AnyTimes().Return("fake-cluster") + p.SubscriptionID().AnyTimes().Return("123") + m.CreateOrUpdate(gomockinternal.AContext(), "group1", "vnet1", "vnet1-to-vnet2", gomockinternal.DiffEq(network.VirtualNetworkPeering{ + VirtualNetworkPeeringPropertiesFormat: &network.VirtualNetworkPeeringPropertiesFormat{ + RemoteVirtualNetwork: &network.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/group2/providers/Microsoft.Network/virtualNetworks/vnet2"), + }, + }, + })) + m.CreateOrUpdate(gomockinternal.AContext(), "group2", "vnet2", "vnet2-to-vnet1", gomockinternal.DiffEq(network.VirtualNetworkPeering{ + VirtualNetworkPeeringPropertiesFormat: &network.VirtualNetworkPeeringPropertiesFormat{ + RemoteVirtualNetwork: &network.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/group1/providers/Microsoft.Network/virtualNetworks/vnet1"), + }, + }, + })) + m.CreateOrUpdate(gomockinternal.AContext(), "group1", "vnet1", "vnet1-to-vnet3", gomockinternal.DiffEq(network.VirtualNetworkPeering{ + VirtualNetworkPeeringPropertiesFormat: &network.VirtualNetworkPeeringPropertiesFormat{ + RemoteVirtualNetwork: &network.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/group3/providers/Microsoft.Network/virtualNetworks/vnet3"), + }, + }, + })).Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) + }, + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + t.Parallel() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + scopeMock := mock_vnetpeerings.NewMockVnetPeeringScope(mockCtrl) + clientMock := mock_vnetpeerings.NewMockClient(mockCtrl) + + tc.expect(scopeMock.EXPECT(), clientMock.EXPECT()) + + s := &Service{ + Scope: scopeMock, + Client: clientMock, + } + + err := s.Reconcile(context.TODO()) + if tc.expectedError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError(tc.expectedError)) + } else { + g.Expect(err).NotTo(HaveOccurred()) + } + }) + } +} + +func TestDeleteVnetPeerings(t *testing.T) { + testcases := []struct { + name string + expectedError string + expect func(p *mock_vnetpeerings.MockVnetPeeringScopeMockRecorder, m *mock_vnetpeerings.MockClientMockRecorder) + }{ + { + name: "delete one peering", + expectedError: "", + expect: func(p *mock_vnetpeerings.MockVnetPeeringScopeMockRecorder, m *mock_vnetpeerings.MockClientMockRecorder) { + p.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + p.VnetPeeringSpecs().Return([]azure.VnetPeeringSpec{ + { + PeeringName: "vnet1-to-vnet2", + SourceVnetName: "vnet1", + SourceResourceGroup: "group1", + RemoteVnetName: "vnet2", + RemoteResourceGroup: "group2", + }, + }) + p.ClusterName().AnyTimes().Return("fake-cluster") + p.SubscriptionID().AnyTimes().Return("123") + m.Delete(gomockinternal.AContext(), "group1", "vnet1", "vnet1-to-vnet2") + }, + }, + { + name: "delete no peerings", + expectedError: "", + expect: func(p *mock_vnetpeerings.MockVnetPeeringScopeMockRecorder, m *mock_vnetpeerings.MockClientMockRecorder) { + p.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + p.VnetPeeringSpecs().Return([]azure.VnetPeeringSpec{}) + p.ClusterName().AnyTimes().Return("fake-cluster") + p.SubscriptionID().AnyTimes().Return("123") + }, + }, + { + name: "delete even number of peerings", + expectedError: "", + expect: func(p *mock_vnetpeerings.MockVnetPeeringScopeMockRecorder, m *mock_vnetpeerings.MockClientMockRecorder) { + p.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + p.VnetPeeringSpecs().Return([]azure.VnetPeeringSpec{ + { + PeeringName: "vnet1-to-vnet2", + SourceVnetName: "vnet1", + SourceResourceGroup: "group1", + RemoteVnetName: "vnet2", + RemoteResourceGroup: "group2", + }, + { + PeeringName: "vnet2-to-vnet1", + SourceVnetName: "vnet2", + SourceResourceGroup: "group2", + RemoteVnetName: "vnet1", + RemoteResourceGroup: "group1", + }, + }) + p.ClusterName().AnyTimes().Return("fake-cluster") + p.SubscriptionID().AnyTimes().Return("123") + m.Delete(gomockinternal.AContext(), "group1", "vnet1", "vnet1-to-vnet2") + m.Delete(gomockinternal.AContext(), "group2", "vnet2", "vnet2-to-vnet1") + }, + }, + { + name: "delete odd number of peerings", + expectedError: "", + expect: func(p *mock_vnetpeerings.MockVnetPeeringScopeMockRecorder, m *mock_vnetpeerings.MockClientMockRecorder) { + p.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + p.VnetPeeringSpecs().Return([]azure.VnetPeeringSpec{ + { + PeeringName: "vnet1-to-vnet2", + SourceVnetName: "vnet1", + SourceResourceGroup: "group1", + RemoteVnetName: "vnet2", + RemoteResourceGroup: "group2", + }, + { + PeeringName: "vnet2-to-vnet1", + SourceVnetName: "vnet2", + SourceResourceGroup: "group2", + RemoteVnetName: "vnet1", + RemoteResourceGroup: "group1", + }, + { + PeeringName: "extra-peering", + SourceVnetName: "vnet3", + SourceResourceGroup: "group3", + RemoteVnetName: "vnet4", + RemoteResourceGroup: "group4", + }, + }) + p.Vnet().AnyTimes().Return(&infrav1.VnetSpec{Name: "vnet2"}) + p.ClusterName().AnyTimes().Return("fake-cluster") + p.SubscriptionID().AnyTimes().Return("123") + m.Delete(gomockinternal.AContext(), "group1", "vnet1", "vnet1-to-vnet2") + m.Delete(gomockinternal.AContext(), "group2", "vnet2", "vnet2-to-vnet1") + m.Delete(gomockinternal.AContext(), "group3", "vnet3", "extra-peering") + }, + }, + { + name: "delete multiple peerings on one vnet", + expectedError: "", + expect: func(p *mock_vnetpeerings.MockVnetPeeringScopeMockRecorder, m *mock_vnetpeerings.MockClientMockRecorder) { + p.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + p.VnetPeeringSpecs().Return([]azure.VnetPeeringSpec{ + { + PeeringName: "vnet1-to-vnet2", + SourceVnetName: "vnet1", + SourceResourceGroup: "group1", + RemoteVnetName: "vnet2", + RemoteResourceGroup: "group2", + }, + { + PeeringName: "vnet2-to-vnet1", + SourceVnetName: "vnet2", + SourceResourceGroup: "group2", + RemoteVnetName: "vnet1", + RemoteResourceGroup: "group1", + }, + { + PeeringName: "vnet1-to-vnet3", + SourceVnetName: "vnet1", + SourceResourceGroup: "group1", + RemoteVnetName: "vnet3", + RemoteResourceGroup: "group3", + }, + { + PeeringName: "vnet3-to-vnet1", + SourceVnetName: "vnet3", + SourceResourceGroup: "group3", + RemoteVnetName: "vnet1", + RemoteResourceGroup: "group1", + }, + }) + p.ClusterName().AnyTimes().Return("fake-cluster") + p.SubscriptionID().AnyTimes().Return("123") + m.Delete(gomockinternal.AContext(), "group1", "vnet1", "vnet1-to-vnet2") + m.Delete(gomockinternal.AContext(), "group2", "vnet2", "vnet2-to-vnet1") + m.Delete(gomockinternal.AContext(), "group1", "vnet1", "vnet1-to-vnet3") + m.Delete(gomockinternal.AContext(), "group3", "vnet3", "vnet3-to-vnet1") + }, + }, + { + name: "error in deleting peering where loop terminates prematurely", + expectedError: "failed to delete peering vnet1-to-vnet3 in vnet vnet1 and resource group group1: #: Internal Server Error: StatusCode=500", + expect: func(p *mock_vnetpeerings.MockVnetPeeringScopeMockRecorder, m *mock_vnetpeerings.MockClientMockRecorder) { + p.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + p.VnetPeeringSpecs().Return([]azure.VnetPeeringSpec{ + { + PeeringName: "vnet1-to-vnet2", + SourceVnetName: "vnet1", + SourceResourceGroup: "group1", + RemoteVnetName: "vnet2", + RemoteResourceGroup: "group2", + }, + { + PeeringName: "vnet2-to-vnet1", + SourceVnetName: "vnet2", + SourceResourceGroup: "group2", + RemoteVnetName: "vnet1", + RemoteResourceGroup: "group1", + }, + { + PeeringName: "vnet1-to-vnet3", + SourceVnetName: "vnet1", + SourceResourceGroup: "group1", + RemoteVnetName: "vnet3", + RemoteResourceGroup: "group3", + }, + { + PeeringName: "vnet3-to-vnet1", + SourceVnetName: "vnet3", + SourceResourceGroup: "group3", + RemoteVnetName: "vnet1", + RemoteResourceGroup: "group1", + }, + }) + p.Vnet().AnyTimes().Return(&infrav1.VnetSpec{ + Name: "vnet1", + ResourceGroup: "group1", + }) + p.ClusterName().AnyTimes().Return("fake-cluster") + p.SubscriptionID().AnyTimes().Return("123") + m.Delete(gomockinternal.AContext(), "group1", "vnet1", "vnet1-to-vnet2") + m.Delete(gomockinternal.AContext(), "group2", "vnet2", "vnet2-to-vnet1") + m.Delete(gomockinternal.AContext(), "group1", "vnet1", "vnet1-to-vnet3").Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) + }, + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + t.Parallel() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + scopeMock := mock_vnetpeerings.NewMockVnetPeeringScope(mockCtrl) + clientMock := mock_vnetpeerings.NewMockClient(mockCtrl) + + tc.expect(scopeMock.EXPECT(), clientMock.EXPECT()) + + s := &Service{ + Scope: scopeMock, + Client: clientMock, + } + + err := s.Delete(context.TODO()) + if tc.expectedError != "" { + fmt.Printf("\nExpected error:\t%s\n", tc.expectedError) + fmt.Printf("\nActual error:\t%s\n", err.Error()) + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError(tc.expectedError)) + } else { + g.Expect(err).NotTo(HaveOccurred()) + } + }) + } +} diff --git a/azure/types.go b/azure/types.go index bf2202713e2..c4c997fb590 100644 --- a/azure/types.go +++ b/azure/types.go @@ -107,6 +107,16 @@ type VNetSpec struct { ResourceGroup string Name string CIDRs []string + Peerings []infrav1.VnetPeeringSpec +} + +// VnetPeeringSpec defines the specification for a virtual network peering. +type VnetPeeringSpec struct { + SourceResourceGroup string + SourceVnetName string + RemoteResourceGroup string + RemoteVnetName string + PeeringName string } // RoleAssignmentSpec defines the specification for a Role Assignment. @@ -195,11 +205,16 @@ type TagsSpec struct { // PrivateDNSSpec defines the specification for a private DNS zone. type PrivateDNSSpec struct { - ZoneName string + ZoneName string + Links []PrivateDNSLinkSpec + Records []infrav1.AddressRecord +} + +// PrivateDNSLinkSpec defines the specification for a virtual network link in a private DNS zone. +type PrivateDNSLinkSpec struct { VNetName string VNetResourceGroup string LinkName string - Records []infrav1.AddressRecord } // AvailabilitySetSpec defines the specification for an availability set. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml index 362f3614df8..06e9dd9ed21 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml @@ -1931,6 +1931,26 @@ spec: name: description: Name defines a name for the virtual network resource. type: string + peerings: + description: Peerings defines a list of peerings of the newly + created virtual network with existing virtual networks. + items: + description: VnetPeeringSpec specifies an existing remote + virtual network to peer with the AzureCluster's virtual + network. + properties: + remoteVnetName: + description: RemoteVnetName defines name of the remote + virtual network. + type: string + resourceGroup: + description: ResourceGroup is the resource group name + of the remote virtual network. + type: string + required: + - remoteVnetName + type: object + type: array resourceGroup: description: ResourceGroup is the name of the resource group of the existing virtual network or the resource group where diff --git a/controllers/azurecluster_reconciler.go b/controllers/azurecluster_reconciler.go index cdd42711710..1fd00a315da 100644 --- a/controllers/azurecluster_reconciler.go +++ b/controllers/azurecluster_reconciler.go @@ -35,6 +35,7 @@ import ( "sigs.k8s.io/cluster-api-provider-azure/azure/services/securitygroups" "sigs.k8s.io/cluster-api-provider-azure/azure/services/subnets" "sigs.k8s.io/cluster-api-provider-azure/azure/services/virtualnetworks" + "sigs.k8s.io/cluster-api-provider-azure/azure/services/vnetpeerings" "sigs.k8s.io/cluster-api-provider-azure/util/tele" ) @@ -52,6 +53,7 @@ type azureClusterService struct { bastionSvc azure.Reconciler skuCache *resourceskus.Cache natGatewaySvc azure.Reconciler + peeringsSvc azure.Reconciler } // newAzureClusterService populates all the services based on input scope. @@ -74,6 +76,7 @@ func newAzureClusterService(scope *scope.ClusterScope) (*azureClusterService, er privateDNSSvc: privatedns.New(scope), bastionSvc: bastionhosts.New(scope), skuCache: skuCache, + peeringsSvc: vnetpeerings.New(scope), }, nil } @@ -119,6 +122,10 @@ func (s *azureClusterService) Reconcile(ctx context.Context) error { return errors.Wrapf(err, "failed to reconcile subnet") } + if err := s.peeringsSvc.Reconcile(ctx); err != nil { + return errors.Wrap(err, "failed to reconcile peerings") + } + if err := s.loadBalancerSvc.Reconcile(ctx); err != nil { return errors.Wrap(err, "failed to reconcile load balancer") } @@ -153,6 +160,10 @@ func (s *azureClusterService) Delete(ctx context.Context) error { return errors.Wrap(err, "failed to delete load balancer") } + if err := s.peeringsSvc.Delete(ctx); err != nil { + return errors.Wrap(err, "failed to delete peerings") + } + if err := s.subnetsSvc.Delete(ctx); err != nil { return errors.Wrap(err, "failed to delete subnet") } diff --git a/controllers/azurecluster_reconciler_test.go b/controllers/azurecluster_reconciler_test.go index 27d682275cd..a469ff84003 100644 --- a/controllers/azurecluster_reconciler_test.go +++ b/controllers/azurecluster_reconciler_test.go @@ -33,7 +33,7 @@ import ( gomockinternal "sigs.k8s.io/cluster-api-provider-azure/internal/test/matchers/gomock" ) -type expect func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder) +type expect func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder, peer *mock_azure.MockReconcilerMockRecorder) func TestAzureClusterReconcilerDelete(t *testing.T) { cases := map[string]struct { @@ -42,26 +42,27 @@ func TestAzureClusterReconcilerDelete(t *testing.T) { }{ "Resource Group is deleted successfully": { expectedError: "", - expect: func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder) { + expect: func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder, peer *mock_azure.MockReconcilerMockRecorder) { gomock.InOrder( grp.Delete(gomockinternal.AContext()).Return(nil)) }, }, "Resource Group delete fails": { expectedError: "failed to delete resource group: internal error", - expect: func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder) { + expect: func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder, peer *mock_azure.MockReconcilerMockRecorder) { gomock.InOrder( grp.Delete(gomockinternal.AContext()).Return(errors.New("internal error"))) }, }, "Resource Group not owned by cluster": { expectedError: "", - expect: func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder) { + expect: func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder, peer *mock_azure.MockReconcilerMockRecorder) { gomock.InOrder( grp.Delete(gomockinternal.AContext()).Return(azure.ErrNotOwned), bastion.Delete(gomockinternal.AContext()), dns.Delete(gomockinternal.AContext()), lb.Delete(gomockinternal.AContext()), + peer.Delete(gomockinternal.AContext()), sn.Delete(gomockinternal.AContext()), natg.Delete(gomockinternal.AContext()), pip.Delete(gomockinternal.AContext()), @@ -73,7 +74,7 @@ func TestAzureClusterReconcilerDelete(t *testing.T) { }, "Load Balancer delete fails": { expectedError: "failed to delete load balancer: some error happened", - expect: func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder) { + expect: func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder, peer *mock_azure.MockReconcilerMockRecorder) { gomock.InOrder( grp.Delete(gomockinternal.AContext()).Return(azure.ErrNotOwned), bastion.Delete(gomockinternal.AContext()), @@ -84,12 +85,13 @@ func TestAzureClusterReconcilerDelete(t *testing.T) { }, "Route table delete fails": { expectedError: "failed to delete route table: some error happened", - expect: func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder) { + expect: func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder, peer *mock_azure.MockReconcilerMockRecorder) { gomock.InOrder( grp.Delete(gomockinternal.AContext()).Return(azure.ErrNotOwned), bastion.Delete(gomockinternal.AContext()), dns.Delete(gomockinternal.AContext()), lb.Delete(gomockinternal.AContext()), + peer.Delete(gomockinternal.AContext()), sn.Delete(gomockinternal.AContext()), pip.Delete(gomockinternal.AContext()), natg.Delete(gomockinternal.AContext()), @@ -117,8 +119,9 @@ func TestAzureClusterReconcilerDelete(t *testing.T) { lbMock := mock_azure.NewMockReconciler(mockCtrl) dnsMock := mock_azure.NewMockReconciler(mockCtrl) bastionMock := mock_azure.NewMockReconciler(mockCtrl) + peeringsMock := mock_azure.NewMockReconciler(mockCtrl) - tc.expect(groupsMock.EXPECT(), vnetMock.EXPECT(), sgMock.EXPECT(), rtMock.EXPECT(), subnetsMock.EXPECT(), natGatewaysMock.EXPECT(), publicIPMock.EXPECT(), lbMock.EXPECT(), dnsMock.EXPECT(), bastionMock.EXPECT()) + tc.expect(groupsMock.EXPECT(), vnetMock.EXPECT(), sgMock.EXPECT(), rtMock.EXPECT(), subnetsMock.EXPECT(), natGatewaysMock.EXPECT(), publicIPMock.EXPECT(), lbMock.EXPECT(), dnsMock.EXPECT(), bastionMock.EXPECT(), peeringsMock.EXPECT()) s := &azureClusterService{ scope: &scope.ClusterScope{ @@ -134,6 +137,7 @@ func TestAzureClusterReconcilerDelete(t *testing.T) { loadBalancerSvc: lbMock, privateDNSSvc: dnsMock, bastionSvc: bastionMock, + peeringsSvc: peeringsMock, skuCache: resourceskus.NewStaticCache([]compute.ResourceSku{}, ""), } diff --git a/docs/book/src/topics/custom-vnet.md b/docs/book/src/topics/custom-vnet.md index 39db6e357f1..b2ee80d8f35 100644 --- a/docs/book/src/topics/custom-vnet.md +++ b/docs/book/src/topics/custom-vnet.md @@ -30,6 +30,42 @@ If providing an existing vnet and subnets with existing network security groups, The pre-existing vnet can be in the same resource group or a different resource group in the same subscription as the target cluster. When deleting the `AzureCluster`, the vnet and resource group will only be deleted if they are "managed" by capz, ie. they were created during cluster deployment. Pre-existing vnets and resource groups will *not* be deleted. +## Virtual Network Peering + +Alternatively, pre-existing vnets can be peered with a cluster's newly created vnets by specifying each vnet by name and resource group. + +```yaml +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: AzureCluster +metadata: + name: cluster-vnet-peering + namespace: default +spec: + location: southcentralus + networkSpec: + vnet: + name: my-vnet + cidrBlocks: + - 10.255.0.0/16 + peerings: + - resourceGroup: vnet-peering-rg + remoteVnetName: existing-vnet-1 + - resourceGroup: vnet-peering-rg + remoteVnetName: existing-vnet-2 + subnets: + - name: my-subnet-cp + role: control-plane + cidrBlocks: + - 10.255.0.0/24 + - name: my-subnet-node + role: node + cidrBlocks: + - 10.255.1.0/24 + resourceGroup: cluster-vnet-peering + ``` + +Currently, only virtual networks on the same subscription can be peered. Also, note that when creating workload clusters with internal load balancers, the management cluster must be in the same VNet or a peered VNet. See [here](https://capz.sigs.k8s.io/topics/api-server-endpoint.html#warning) for more details. + ## Custom Network Spec It is also possible to customize the vnet to be created without providing an already existing vnet. To do so, simply modify the `AzureCluster` `NetworkSpec` as desired. Here is an illustrative example of a cluster with a customized vnet address space (CIDR) and customized subnets: diff --git a/templates/test/ci/cluster-template-prow-custom-vnet.yaml b/templates/test/ci/cluster-template-prow-custom-vnet.yaml index d2c2a976566..b60d88c7308 100644 --- a/templates/test/ci/cluster-template-prow-custom-vnet.yaml +++ b/templates/test/ci/cluster-template-prow-custom-vnet.yaml @@ -36,18 +36,13 @@ spec: location: ${AZURE_LOCATION} networkSpec: subnets: - - name: ${CLUSTER_NAME}-controlplane-subnet + - name: ${AZURE_CUSTOM_VNET_NAME}-controlplane-subnet role: control-plane - securityGroup: - name: control-plane-nsg - - name: ${CLUSTER_NAME}-node-subnet + - name: ${AZURE_CUSTOM_VNET_NAME}-node-subnet role: node - routeTable: - name: node-routetable - securityGroup: - name: node-nsg vnet: - name: ${AZURE_VNET_NAME:=${CLUSTER_NAME}-vnet} + name: ${AZURE_CUSTOM_VNET_NAME} + resourceGroup: ${AZURE_RESOURCE_GROUP} resourceGroup: ${AZURE_RESOURCE_GROUP:=${CLUSTER_NAME}} subscriptionID: ${AZURE_SUBSCRIPTION_ID} --- diff --git a/templates/test/ci/cluster-template-prow-private.yaml b/templates/test/ci/cluster-template-prow-private.yaml index a6e9b10a010..ffdbf70caa6 100644 --- a/templates/test/ci/cluster-template-prow-private.yaml +++ b/templates/test/ci/cluster-template-prow-private.yaml @@ -52,18 +52,17 @@ spec: - ${AZURE_CP_SUBNET_CIDR} name: private-cp-subnet role: control-plane - securityGroup: - name: control-plane-nsg - cidrBlocks: - ${AZURE_NODE_SUBNET_CIDR} name: private-node-subnet role: node - routeTable: - name: node-routetable - securityGroup: - name: node-nsg vnet: + cidrBlocks: + - ${AZURE_PRIVATE_VNET_CIDR} name: ${AZURE_VNET_NAME} + peerings: + - remoteVnetName: ${AZURE_CUSTOM_VNET_NAME} + resourceGroup: ${AZURE_RESOURCE_GROUP} resourceGroup: ${AZURE_RESOURCE_GROUP:=${CLUSTER_NAME}} subscriptionID: ${AZURE_SUBSCRIPTION_ID} --- diff --git a/templates/test/ci/prow-custom-vnet/patches/custom-vnet.yaml b/templates/test/ci/prow-custom-vnet/patches/custom-vnet.yaml index e9caf68dc66..f1f45c952ff 100644 --- a/templates/test/ci/prow-custom-vnet/patches/custom-vnet.yaml +++ b/templates/test/ci/prow-custom-vnet/patches/custom-vnet.yaml @@ -5,15 +5,10 @@ metadata: spec: networkSpec: vnet: - name: ${AZURE_VNET_NAME:=${CLUSTER_NAME}-vnet} + resourceGroup: ${AZURE_RESOURCE_GROUP} + name: ${AZURE_CUSTOM_VNET_NAME} subnets: - - name: ${CLUSTER_NAME}-controlplane-subnet + - name: ${AZURE_CUSTOM_VNET_NAME}-controlplane-subnet role: control-plane - securityGroup: - name: control-plane-nsg - - name: ${CLUSTER_NAME}-node-subnet + - name: ${AZURE_CUSTOM_VNET_NAME}-node-subnet role: node - securityGroup: - name: node-nsg - routeTable: - name: node-routetable diff --git a/templates/test/ci/prow-private/kustomization.yaml b/templates/test/ci/prow-private/kustomization.yaml index 8aec9551656..88619734703 100644 --- a/templates/test/ci/prow-private/kustomization.yaml +++ b/templates/test/ci/prow-private/kustomization.yaml @@ -8,7 +8,7 @@ patchesStrategicMerge: - ../patches/tags.yaml - ../patches/controller-manager.yaml - ../patches/cluster-cni.yaml - - patches/custom-vnet.yaml + - patches/vnet-peerings.yaml configMapGenerator: - name: cni-${CLUSTER_NAME}-calico files: diff --git a/templates/test/ci/prow-private/patches/custom-vnet.yaml b/templates/test/ci/prow-private/patches/vnet-peerings.yaml similarity index 61% rename from templates/test/ci/prow-private/patches/custom-vnet.yaml rename to templates/test/ci/prow-private/patches/vnet-peerings.yaml index eb7400d59a0..cf5101d1d49 100644 --- a/templates/test/ci/prow-private/patches/custom-vnet.yaml +++ b/templates/test/ci/prow-private/patches/vnet-peerings.yaml @@ -4,26 +4,25 @@ metadata: name: ${CLUSTER_NAME} spec: networkSpec: + apiServerLB: + frontendIPs: + - name: ${CLUSTER_NAME}-internal-lb-frontend + privateIP: ${AZURE_INTERNAL_LB_IP} + name: ${CLUSTER_NAME}-internal-lb + type: Internal vnet: name: ${AZURE_VNET_NAME} + cidrBlocks: + - ${AZURE_PRIVATE_VNET_CIDR} + peerings: + - resourceGroup: ${AZURE_RESOURCE_GROUP} + remoteVnetName: ${AZURE_CUSTOM_VNET_NAME} subnets: - name: private-cp-subnet role: control-plane - securityGroup: - name: control-plane-nsg cidrBlocks: - ${AZURE_CP_SUBNET_CIDR} - name: private-node-subnet role: node - securityGroup: - name: node-nsg - routeTable: - name: node-routetable cidrBlocks: - - ${AZURE_NODE_SUBNET_CIDR} - apiServerLB: - name: ${CLUSTER_NAME}-internal-lb - type: Internal - frontendIPs: - - name: ${CLUSTER_NAME}-internal-lb-frontend - privateIP: ${AZURE_INTERNAL_LB_IP} + - ${AZURE_NODE_SUBNET_CIDR} \ No newline at end of file diff --git a/test/e2e/azure_privatecluster.go b/test/e2e/azure_privatecluster.go index d93adcd5d6a..76548d29100 100644 --- a/test/e2e/azure_privatecluster.go +++ b/test/e2e/azure_privatecluster.go @@ -40,7 +40,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/utils/pointer" - "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" "sigs.k8s.io/cluster-api-provider-azure/azure" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" capi_e2e "sigs.k8s.io/cluster-api/test/e2e" @@ -128,7 +127,11 @@ func AzurePrivateClusterSpec(ctx context.Context, inputGetter func() AzurePrivat By("Creating a private workload cluster") clusterName = fmt.Sprintf("capz-e2e-%s-%s", util.RandomString(6), "private") - Expect(os.Setenv(AzureInternalLBIP, "10.128.0.100")).NotTo(HaveOccurred()) + Expect(os.Setenv(AzureVNetName, clusterName+"-vnet")).NotTo(HaveOccurred()) + Expect(os.Setenv(AzureVNetCidr, "10.255.0.0/16")).NotTo(HaveOccurred()) + Expect(os.Setenv(AzureInternalLBIP, "10.255.0.100")).NotTo(HaveOccurred()) + Expect(os.Setenv(AzureCPSubnetCidr, "10.255.0.0/24")).NotTo(HaveOccurred()) + Expect(os.Setenv(AzureNodeSubnetCidr, "10.255.1.0/24")).NotTo(HaveOccurred()) result := &clusterctl.ApplyClusterTemplateAndWaitResult{} clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{ ClusterProxy: publicClusterProxy, @@ -220,7 +223,7 @@ func AzurePrivateClusterSpec(ctx context.Context, inputGetter func() AzurePrivat } // SetupExistingVNet creates a resource group and a VNet to be used by a workload cluster. -func SetupExistingVNet(ctx context.Context, vnetCidr string, cpSubnetCidrs, nodeSubnetCidrs map[string]string) func() { +func SetupExistingVNet(ctx context.Context, vnetCidr string, cpSubnetCidrs, nodeSubnetCidrs map[string]string, bastionSubnetName, bastionSubnetCidr string) func() { By("creating Azure clients with the workload cluster's subscription") settings, err := auth.GetSettingsFromEnvironment() Expect(err).NotTo(HaveOccurred()) @@ -344,12 +347,12 @@ func SetupExistingVNet(ctx context.Context, vnetCidr string, cpSubnetCidrs, node // Create the AzureBastion subnet. subnets = append(subnets, network.Subnet{ SubnetPropertiesFormat: &network.SubnetPropertiesFormat{ - AddressPrefix: pointer.StringPtr(v1beta1.DefaultAzureBastionSubnetCIDR), + AddressPrefix: pointer.StringPtr(bastionSubnetCidr), }, - Name: pointer.StringPtr(v1beta1.DefaultAzureBastionSubnetName), + Name: pointer.StringPtr(bastionSubnetName), }) - vnetFuture, err := vnetClient.CreateOrUpdate(ctx, groupName, os.Getenv(AzureVNetName), network.VirtualNetwork{ + vnetFuture, err := vnetClient.CreateOrUpdate(ctx, groupName, os.Getenv(AzureCustomVNetName), network.VirtualNetwork{ Location: pointer.StringPtr(os.Getenv(AzureLocation)), VirtualNetworkPropertiesFormat: &network.VirtualNetworkPropertiesFormat{ AddressSpace: &network.AddressSpace{ @@ -366,8 +369,8 @@ func SetupExistingVNet(ctx context.Context, vnetCidr string, cpSubnetCidrs, node Expect(err).To(BeNil()) return func() { - Logf("deleting an existing virtual network %q", os.Getenv(AzureVNetName)) - vFuture, err := vnetClient.Delete(ctx, groupName, os.Getenv(AzureVNetName)) + Logf("deleting an existing virtual network %q", os.Getenv(AzureCustomVNetName)) + vFuture, err := vnetClient.Delete(ctx, groupName, os.Getenv(AzureCustomVNetName)) Expect(err).NotTo(HaveOccurred()) Expect(vFuture.WaitForCompletionRef(ctx, vnetClient.Client)).ToNot(HaveOccurred()) diff --git a/test/e2e/azure_test.go b/test/e2e/azure_test.go index 80b83633e80..cb97f140184 100644 --- a/test/e2e/azure_test.go +++ b/test/e2e/azure_test.go @@ -141,15 +141,14 @@ var _ = Describe("Workload cluster creation", func() { It("Creates a public management cluster in the same vnet", func() { clusterName = getClusterName(clusterNamePrefix, "public-custom-vnet") Context("Creating a custom virtual network", func() { - Expect(os.Setenv(AzureVNetName, "custom-vnet")).NotTo(HaveOccurred()) - cpCIDR := "10.128.0.0/16" - Expect(os.Setenv(AzureCPSubnetCidr, cpCIDR)).NotTo(HaveOccurred()) - nodeCIDR := "10.129.0.0/16" - Expect(os.Setenv(AzureNodeSubnetCidr, nodeCIDR)).NotTo(HaveOccurred()) + Expect(os.Setenv(AzureCustomVNetName, "custom-vnet")).NotTo(HaveOccurred()) additionalCleanup = SetupExistingVNet(ctx, - "10.0.0.0/8", - map[string]string{fmt.Sprintf("%s-controlplane-subnet", clusterName): "10.0.0.0/16", "private-cp-subnet": cpCIDR}, - map[string]string{fmt.Sprintf("%s-node-subnet", clusterName): "10.1.0.0/16", "private-node-subnet": nodeCIDR}) + "10.0.0.0/16", + map[string]string{fmt.Sprintf("%s-controlplane-subnet", os.Getenv(AzureCustomVNetName)): "10.0.0.0/24"}, + map[string]string{fmt.Sprintf("%s-node-subnet", os.Getenv(AzureCustomVNetName)): "10.0.1.0/24"}, + fmt.Sprintf("%s-azure-bastion-subnet", os.Getenv(AzureCustomVNetName)), + "10.0.2.0/24", + ) }) clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{ diff --git a/test/e2e/common.go b/test/e2e/common.go index ce5062b1a26..c0375c45913 100644 --- a/test/e2e/common.go +++ b/test/e2e/common.go @@ -49,8 +49,10 @@ const ( AzureLocation = "AZURE_LOCATION" AzureResourceGroup = "AZURE_RESOURCE_GROUP" AzureVNetName = "AZURE_VNET_NAME" + AzureCustomVNetName = "AZURE_CUSTOM_VNET_NAME" AzureInternalLBIP = "AZURE_INTERNAL_LB_IP" AzureCPSubnetCidr = "AZURE_CP_SUBNET_CIDR" + AzureVNetCidr = "AZURE_PRIVATE_VNET_CIDR" AzureNodeSubnetCidr = "AZURE_NODE_SUBNET_CIDR" MultiTenancyIdentityName = "MULTI_TENANCY_IDENTITY_NAME" ClusterIdentityName = "CLUSTER_IDENTITY_NAME"