diff --git a/azure/services/privatedns/spec.go b/azure/services/privatedns/spec.go new file mode 100644 index 00000000000..5e1006dcc0d --- /dev/null +++ b/azure/services/privatedns/spec.go @@ -0,0 +1,188 @@ +/* +Copyright 2022 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 privatedns + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns" + "github.com/Azure/go-autorest/autorest/to" + "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/azure/converters" + "sigs.k8s.io/cluster-api-provider-azure/util/tele" +) + +// ZoneSpec defines the specification for private dns zone. +type ZoneSpec struct { + Name string + ResourceGroup string + ClusterName string + AdditionalTags infrav1.Tags +} + +// LinkSpec defines the specification for a virtual network link in a private DNS zone. +type LinkSpec struct { + Name string + ZoneName string + SubscriptionID string + VNetResourceGroup string + VNetName string + ResourceGroup string + ClusterName string + AdditionalTags infrav1.Tags +} + +type RecordSpec struct { + Record infrav1.AddressRecord + ZoneName string + ResourceGroup string +} + +// ResourceName returns the name of the private dns zone. +func (z ZoneSpec) ResourceName() string { + return z.Name +} + +// OwnerResourceName is a no-op for private dns zone. +func (z ZoneSpec) OwnerResourceName() string { + return "" +} + +// ResourceGroupName returns the name of the resource group of the private dns zone. +func (z ZoneSpec) ResourceGroupName() string { + return z.ResourceGroup +} + +// Parameters returns the parameters for the private dns zone. +func (z ZoneSpec) Parameters(existing interface{}) (params interface{}, err error) { + _, log, done := tele.StartSpanWithLogger(context.TODO(), "privatedns.ZoneSpec.Parameters") + defer done() + + if existing != nil { + zone, ok := existing.(privatedns.PrivateZone) + if !ok { + return nil, errors.Errorf("%T is not a privatedns.PrivateZone", existing) + } + if !isManaged(zone.Tags, z.ClusterName) { + log.V(1).Info("Skipping reconciliation of unmanaged private DNS zone", "private DNS", zone.Name) + log.V(1).Info("Tag the DNS manually from azure to manage it with capz."+ + "Please see https://capz.sigs.k8s.io/topics/custom-dns.html#manage-dns-via-capz-tool", "private DNS", zone.Name) + return nil, nil + } + } + + return privatedns.PrivateZone{ + Location: to.StringPtr(azure.Global), + Tags: converters.TagsToMap(infrav1.Build(infrav1.BuildParams{ + ClusterName: z.ClusterName, + Lifecycle: infrav1.ResourceLifecycleOwned, + Additional: z.AdditionalTags, + })), + }, nil +} + +// ResourceName returns the name of the virtual network link. +func (l LinkSpec) ResourceName() string { + return l.Name +} + +// OwnerResourceName returns the zone name of the virtual network link. +func (l LinkSpec) OwnerResourceName() string { + return l.ZoneName +} + +// ResourceGroupName returns the name of the resource group of the virtual network link. +func (l LinkSpec) ResourceGroupName() string { + return l.ResourceGroup +} + +// Parameters returns the parameters for the virtual network link. +func (l LinkSpec) Parameters(existing interface{}) (params interface{}, err error) { + _, log, done := tele.StartSpanWithLogger(context.TODO(), "privatedns.LinkSpec.Parameters") + defer done() + + if existing != nil { + link, ok := existing.(privatedns.VirtualNetworkLink) + if !ok { + return nil, errors.Errorf("%T is not a privatedns.VirtualNetworkLink", existing) + } + if !isManaged(link.Tags, l.ClusterName) { + log.V(2).Info("Skipping vnet link reconciliation for unmanaged vnet link", "vnet link", + l.Name, "private dns zone", l.ZoneName) + return nil, nil + } + } + + return privatedns.VirtualNetworkLink{ + VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ + VirtualNetwork: &privatedns.SubResource{ + ID: to.StringPtr(azure.VNetID(l.SubscriptionID, l.VNetResourceGroup, l.VNetName)), + }, + RegistrationEnabled: to.BoolPtr(false), + }, + Location: to.StringPtr(azure.Global), + Tags: converters.TagsToMap(infrav1.Build(infrav1.BuildParams{ + ClusterName: l.ClusterName, + Lifecycle: infrav1.ResourceLifecycleOwned, + Additional: l.AdditionalTags, + })), + }, nil +} + +// ResourceName returns the name of the virtual network link. +func (r RecordSpec) ResourceName() string { + return r.Record.Hostname +} + +// OwnerResourceName returns the zone name of the virtual network link. +func (r RecordSpec) OwnerResourceName() string { + return r.ZoneName +} + +// ResourceGroupName returns the name of the resource group of the virtual network link. +func (r RecordSpec) ResourceGroupName() string { + return r.ResourceGroup +} + +// Parameters returns the parameters for the virtual network link. +func (r RecordSpec) Parameters(existing interface{}) (params interface{}, err error) { + set := privatedns.RecordSet{ + RecordSetProperties: &privatedns.RecordSetProperties{ + TTL: to.Int64Ptr(300), + }, + } + recordType := converters.GetRecordType(r.Record.IP) + if recordType == privatedns.A { + set.RecordSetProperties.ARecords = &[]privatedns.ARecord{{ + Ipv4Address: &r.Record.IP, + }} + } else if recordType == privatedns.AAAA { + set.RecordSetProperties.AaaaRecords = &[]privatedns.AaaaRecord{{ + Ipv6Address: &r.Record.IP, + }} + } + + return set, nil +} + +// isManaged returns true if tagsMap contains an owner tag for the cluster. +func isManaged(tagsMap map[string]*string, clusterName string) bool { + tags := converters.MapToTags(tagsMap) + return tags.HasOwned(clusterName) +} diff --git a/azure/services/privatedns/spec_test.go b/azure/services/privatedns/spec_test.go new file mode 100644 index 00000000000..1ba54e8395f --- /dev/null +++ b/azure/services/privatedns/spec_test.go @@ -0,0 +1,320 @@ +/* +Copyright 2022 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 privatedns + +import ( + "testing" + + "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns" + "github.com/Azure/go-autorest/autorest/to" + . "github.com/onsi/gomega" + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" + "sigs.k8s.io/cluster-api-provider-azure/azure" +) + +var ( + zoneSpec = ZoneSpec{ + Name: "my-zone", + ResourceGroup: "my-rg", + ClusterName: "my-cluster", + AdditionalTags: nil, + } + + linkSpec = LinkSpec{ + Name: "my-link", + ZoneName: "my-zone", + SubscriptionID: "123", + VNetResourceGroup: "my-vnet-rg", + VNetName: "my-vnet", + ResourceGroup: "my-rg", + ClusterName: "my-cluster", + AdditionalTags: nil, + } + + recordSpec = RecordSpec{ + Record: infrav1.AddressRecord{Hostname: "privatednsHostname", IP: "10.0.0.8"}, + ZoneName: "my-zone", + ResourceGroup: "my-rg", + } + + recordSpecIpv6 = RecordSpec{ + Record: infrav1.AddressRecord{Hostname: "privatednsHostname", IP: "2603:1030:805:2::b"}, + ZoneName: "my-zone", + ResourceGroup: "my-rg", + } +) + +func TestZoneSpec_ResourceName(t *testing.T) { + g := NewWithT(t) + g.Expect(zoneSpec.ResourceName()).Should(Equal("my-zone")) +} + +func TestZoneSpec_ResourceGroupName(t *testing.T) { + g := NewWithT(t) + g.Expect(zoneSpec.ResourceGroupName()).Should(Equal("my-rg")) +} + +func TestZoneSpec_OwnerResourceName(t *testing.T) { + g := NewWithT(t) + g.Expect(zoneSpec.OwnerResourceName()).Should(Equal("")) +} + +func TestZoneSpec_Parameters(t *testing.T) { + testcases := []struct { + name string + spec ZoneSpec + existing interface{} + expect func(g *WithT, result interface{}) + expectedError string + }{ + { + name: "new private dns zone", + expectedError: "", + spec: zoneSpec, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(Equal(privatedns.PrivateZone{ + Location: to.StringPtr(azure.Global), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + }, + })) + }, + }, + { + name: "existing managed private dns zone", + expectedError: "", + spec: zoneSpec, + existing: privatedns.PrivateZone{Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + }}, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(Equal(privatedns.PrivateZone{ + Location: to.StringPtr(azure.Global), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + }})) + }, + }, + { + name: "existing unmanaged private dns zone", + expectedError: "", + spec: zoneSpec, + existing: privatedns.PrivateZone{}, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(BeNil()) + }, + }, + { + name: "type cast error", + expectedError: "string is not a privatedns.PrivateZone", + spec: zoneSpec, + existing: "I'm not privatedns.PrivateZone", + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + t.Parallel() + + result, err := tc.spec.Parameters(tc.existing) + if tc.expectedError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError(tc.expectedError)) + } else { + g.Expect(err).NotTo(HaveOccurred()) + tc.expect(g, result) + } + }) + } +} + +func TestLinkSpec_ResourceName(t *testing.T) { + g := NewWithT(t) + g.Expect(linkSpec.ResourceName()).Should(Equal("my-link")) +} + +func TestLinkSpec_ResourceGroupName(t *testing.T) { + g := NewWithT(t) + g.Expect(linkSpec.ResourceGroupName()).Should(Equal("my-rg")) +} + +func TestLinkSpec_OwnerResourceName(t *testing.T) { + g := NewWithT(t) + g.Expect(linkSpec.OwnerResourceName()).Should(Equal("my-zone")) +} + +func TestLinkSpec_Parameters(t *testing.T) { + testcases := []struct { + name string + spec LinkSpec + existing interface{} + expect func(g *WithT, result interface{}) + expectedError string + }{ + { + name: "new private dns virtual network link", + expectedError: "", + spec: linkSpec, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(Equal(privatedns.VirtualNetworkLink{ + VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ + VirtualNetwork: &privatedns.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/my-vnet-rg/providers/Microsoft.Network/virtualNetworks/my-vnet"), + }, + RegistrationEnabled: to.BoolPtr(false), + }, + Location: to.StringPtr(azure.Global), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + }, + })) + }, + }, + { + name: "existing managed private virtual network link", + expectedError: "", + spec: linkSpec, + existing: privatedns.VirtualNetworkLink{Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + }}, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(Equal(privatedns.VirtualNetworkLink{ + VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ + VirtualNetwork: &privatedns.SubResource{ + ID: to.StringPtr("/subscriptions/123/resourceGroups/my-vnet-rg/providers/Microsoft.Network/virtualNetworks/my-vnet"), + }, + RegistrationEnabled: to.BoolPtr(false), + }, + Location: to.StringPtr(azure.Global), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + }})) + }, + }, + { + name: "existing unmanaged private dns zone", + expectedError: "", + spec: linkSpec, + existing: privatedns.VirtualNetworkLink{}, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(BeNil()) + }, + }, + { + name: "type cast error", + expectedError: "string is not a privatedns.VirtualNetworkLink", + spec: linkSpec, + existing: "I'm not privatedns.VirtualNetworkLink", + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + t.Parallel() + + result, err := tc.spec.Parameters(tc.existing) + if tc.expectedError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError(tc.expectedError)) + } else { + g.Expect(err).NotTo(HaveOccurred()) + tc.expect(g, result) + } + }) + } +} + +func TestRecordSpec_ResourceName(t *testing.T) { + g := NewWithT(t) + g.Expect(recordSpec.ResourceName()).Should(Equal("privatednsHostname")) +} + +func TestRecordSpec_ResourceGroupName(t *testing.T) { + g := NewWithT(t) + g.Expect(recordSpec.ResourceGroupName()).Should(Equal("my-rg")) +} + +func TestRecordSpec_OwnerResourceName(t *testing.T) { + g := NewWithT(t) + g.Expect(recordSpec.OwnerResourceName()).Should(Equal("my-zone")) +} + +func TestRecordSpec_Parameters(t *testing.T) { + testcases := []struct { + name string + spec RecordSpec + existing interface{} + expect func(g *WithT, result interface{}) + expectedError string + }{ + { + name: "new private dns record for ipv4", + expectedError: "", + spec: recordSpec, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(Equal(privatedns.RecordSet{ + RecordSetProperties: &privatedns.RecordSetProperties{ + TTL: to.Int64Ptr(300), + ARecords: &[]privatedns.ARecord{ + { + Ipv4Address: to.StringPtr("10.0.0.8"), + }, + }, + }, + })) + }, + }, + { + name: "new private dns record for ipv6", + expectedError: "", + spec: recordSpecIpv6, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(Equal(privatedns.RecordSet{ + RecordSetProperties: &privatedns.RecordSetProperties{ + TTL: to.Int64Ptr(300), + AaaaRecords: &[]privatedns.AaaaRecord{ + { + Ipv6Address: to.StringPtr("2603:1030:805:2::b"), + }, + }, + }, + })) + }, + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + t.Parallel() + + result, err := tc.spec.Parameters(tc.existing) + if tc.expectedError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError(tc.expectedError)) + } else { + g.Expect(err).NotTo(HaveOccurred()) + tc.expect(g, result) + } + }) + } +} diff --git a/azure/types.go b/azure/types.go index 0b9db5e6081..63cfdd35c1f 100644 --- a/azure/types.go +++ b/azure/types.go @@ -108,20 +108,6 @@ type TagsSpec struct { Annotation string } -// PrivateDNSSpec defines the specification for a private DNS zone. -type PrivateDNSSpec struct { - 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 -} - // ExtensionSpec defines the specification for a VM or VMScaleSet extension. type ExtensionSpec struct { Name string