diff --git a/azure/services/privatedns/client.go b/azure/services/privatedns/client.go index e3c0fdc80a8..f4f9e2b8a10 100644 --- a/azure/services/privatedns/client.go +++ b/azure/services/privatedns/client.go @@ -28,8 +28,10 @@ import ( // Client wraps go-sdk. type client interface { + GetZone(context.Context, string, string) (privatedns.PrivateZone, error) CreateOrUpdateZone(context.Context, string, string, privatedns.PrivateZone) error DeleteZone(context.Context, string, string) error + GetLink(context.Context, string, string, string) (privatedns.VirtualNetworkLink, error) CreateOrUpdateLink(context.Context, string, string, string, privatedns.VirtualNetworkLink) error DeleteLink(context.Context, string, string, string) error CreateOrUpdateRecordSet(context.Context, string, string, privatedns.RecordType, string, privatedns.RecordSet) error @@ -74,11 +76,21 @@ func newRecordSetsClient(subscriptionID string, baseURI string, authorizer autor return recordsClient } +// GetZone returns a private zone. +func (ac *azureClient) GetZone(ctx context.Context, resourceGroupName, zoneName string) (privatedns.PrivateZone, error) { + ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.AzureClient.GetZone") + defer done() + zone, err := ac.privatezones.Get(ctx, resourceGroupName, zoneName) + if err != nil { + return privatedns.PrivateZone{}, err + } + return zone, nil +} + // CreateOrUpdateZone creates or updates a private zone. func (ac *azureClient) CreateOrUpdateZone(ctx context.Context, resourceGroupName string, zoneName string, zone privatedns.PrivateZone) error { ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.AzureClient.CreateOrUpdateZone") defer done() - future, err := ac.privatezones.CreateOrUpdate(ctx, resourceGroupName, zoneName, zone, "", "") if err != nil { return err @@ -108,6 +120,17 @@ func (ac *azureClient) DeleteZone(ctx context.Context, resourceGroupName, name s return err } +// GetLink returns a vnet link. +func (ac *azureClient) GetLink(ctx context.Context, resourceGroupName, zoneName, vnetLinkName string) (privatedns.VirtualNetworkLink, error) { + ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.AzureClient.GetLink") + defer done() + vnetLink, err := ac.vnetlinks.Get(ctx, resourceGroupName, zoneName, vnetLinkName) + if err != nil { + return privatedns.VirtualNetworkLink{}, err + } + return vnetLink, nil +} + // CreateOrUpdateLink creates or updates a virtual network link to the specified Private DNS zone. func (ac *azureClient) CreateOrUpdateLink(ctx context.Context, resourceGroupName, privateZoneName, name string, link privatedns.VirtualNetworkLink) error { ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.AzureClient.CreateOrUpdateLink") diff --git a/azure/services/privatedns/mock_privatedns/client_mock.go b/azure/services/privatedns/mock_privatedns/client_mock.go index 7bf87e084b6..cf9d71ec70c 100644 --- a/azure/services/privatedns/mock_privatedns/client_mock.go +++ b/azure/services/privatedns/mock_privatedns/client_mock.go @@ -134,3 +134,33 @@ func (mr *MockclientMockRecorder) DeleteZone(arg0, arg1, arg2 interface{}) *gomo mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteZone", reflect.TypeOf((*Mockclient)(nil).DeleteZone), arg0, arg1, arg2) } + +// GetLink mocks base method. +func (m *Mockclient) GetLink(arg0 context.Context, arg1, arg2, arg3 string) (privatedns.VirtualNetworkLink, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLink", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(privatedns.VirtualNetworkLink) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLink indicates an expected call of GetLink. +func (mr *MockclientMockRecorder) GetLink(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLink", reflect.TypeOf((*Mockclient)(nil).GetLink), arg0, arg1, arg2, arg3) +} + +// GetZone mocks base method. +func (m *Mockclient) GetZone(arg0 context.Context, arg1, arg2 string) (privatedns.PrivateZone, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetZone", arg0, arg1, arg2) + ret0, _ := ret[0].(privatedns.PrivateZone) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetZone indicates an expected call of GetZone. +func (mr *MockclientMockRecorder) GetZone(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetZone", reflect.TypeOf((*Mockclient)(nil).GetZone), arg0, arg1, arg2) +} diff --git a/azure/services/privatedns/privatedns.go b/azure/services/privatedns/privatedns.go index c33c4bde653..438710c6f90 100644 --- a/azure/services/privatedns/privatedns.go +++ b/azure/services/privatedns/privatedns.go @@ -19,6 +19,8 @@ package privatedns import ( "context" + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" + "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" @@ -55,15 +57,52 @@ func (s *Service) Reconcile(ctx context.Context) error { zoneSpec := s.Scope.PrivateDNSSpec() if zoneSpec != nil { + // Skip the reconciliation of private DNS zone which is not managed by capz. + isManaged, err := s.isPrivateDNSManaged(ctx, s.Scope.ResourceGroup(), zoneSpec.ZoneName) + if err != nil && !azure.ResourceNotFound(err) { + return errors.Wrapf(err, "could not get private DNS zone state of %s in resource group %s", zoneSpec.ZoneName, s.Scope.ResourceGroup()) + } + // If resource is not found, it means it should be created and hence setting isVnetLinkManaged to true + // will allow the reconciliation to continue + if err != nil && azure.ResourceNotFound(err) { + isManaged = true + } + if !isManaged { + log.V(1).Info("Skipping reconciliation of unmanaged private DNS zone", "private DNS", zoneSpec.ZoneName) + 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", zoneSpec.ZoneName) + return nil + } // Create the private DNS zone. log.V(2).Info("creating private DNS zone", "private dns zone", zoneSpec.ZoneName) - err := s.client.CreateOrUpdateZone(ctx, s.Scope.ResourceGroup(), zoneSpec.ZoneName, privatedns.PrivateZone{Location: to.StringPtr(azure.Global)}) + pDNS := privatedns.PrivateZone{ + Location: to.StringPtr(azure.Global), + Tags: converters.TagsToMap(infrav1.Build(infrav1.BuildParams{ + ClusterName: s.Scope.ClusterName(), + Lifecycle: infrav1.ResourceLifecycleOwned, + Additional: s.Scope.AdditionalTags(), + })), + } + err = s.client.CreateOrUpdateZone(ctx, s.Scope.ResourceGroup(), zoneSpec.ZoneName, pDNS) if err != nil { return errors.Wrapf(err, "failed to create private DNS zone %s", zoneSpec.ZoneName) } log.V(2).Info("successfully created private DNS zone", "private dns zone", zoneSpec.ZoneName) - for _, linkSpec := range zoneSpec.Links { + // If the virtual network link is not managed by capz, skip its reconciliation + isVnetLinkManaged, err := s.isVnetLinkManaged(ctx, s.Scope.ResourceGroup(), zoneSpec.ZoneName, linkSpec.LinkName) + if err != nil && !azure.ResourceNotFound(err) { + return errors.Wrapf(err, "could not get vnet link state of %s in resource group %s", zoneSpec.ZoneName, s.Scope.ResourceGroup()) + } + // If resource is not found, it means it should be created and hence setting isVnetLinkManaged to true + // will allow the reconciliation to continue + if err != nil && azure.ResourceNotFound(err) { + isVnetLinkManaged = true + } + if !isVnetLinkManaged { + log.V(2).Info("Skipping vnet link reconciliation for unmanaged vnet link", "vnet link", linkSpec.LinkName, "private dns zone", zoneSpec.ZoneName) + continue + } // Link each virtual network. log.V(2).Info("creating a virtual network link", "virtual network", linkSpec.VNetName, "private dns zone", zoneSpec.ZoneName) link := privatedns.VirtualNetworkLink{ @@ -74,6 +113,11 @@ func (s *Service) Reconcile(ctx context.Context) error { RegistrationEnabled: to.BoolPtr(false), }, Location: to.StringPtr(azure.Global), + Tags: converters.TagsToMap(infrav1.Build(infrav1.BuildParams{ + ClusterName: s.Scope.ClusterName(), + Lifecycle: infrav1.ResourceLifecycleOwned, + Additional: s.Scope.AdditionalTags(), + })), } err = s.client.CreateOrUpdateLink(ctx, s.Scope.ResourceGroup(), zoneSpec.ZoneName, linkSpec.LinkName, link) if err != nil { @@ -81,7 +125,6 @@ func (s *Service) Reconcile(ctx context.Context) error { } log.V(2).Info("successfully created virtual network link", "virtual network", linkSpec.VNetName, "private dns zone", zoneSpec.ZoneName) } - // Create the record(s). for _, record := range zoneSpec.Records { log.V(2).Info("creating record set", "private dns zone", zoneSpec.ZoneName, "record", record.Hostname) @@ -110,7 +153,7 @@ func (s *Service) Reconcile(ctx context.Context) error { return nil } -// Delete deletes the private zone. +// Delete deletes the private zone and vnet links. func (s *Service) Delete(ctx context.Context) error { ctx, log, done := tele.StartSpanWithLogger(ctx, "privatedns.Service.Delete") defer done() @@ -118,17 +161,33 @@ func (s *Service) Delete(ctx context.Context) error { zoneSpec := s.Scope.PrivateDNSSpec() if zoneSpec != nil { for _, linkSpec := range zoneSpec.Links { - // Remove each virtual network link. + // If the virtual network link is not managed by capz, skip its removal + isVnetLinkManaged, err := s.isVnetLinkManaged(ctx, s.Scope.ResourceGroup(), zoneSpec.ZoneName, linkSpec.LinkName) + if err != nil && !azure.ResourceNotFound(err) { + return errors.Wrapf(err, "could not get vnet link state of %s in resource group %s", zoneSpec.ZoneName, s.Scope.ResourceGroup()) + } + if !isVnetLinkManaged { + log.V(2).Info("Skipping vnet link deletion for unmanaged vnet link", "vnet link", linkSpec.LinkName, "private dns zone", zoneSpec.ZoneName) + continue + } log.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) + 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()) } } - + // Skip the deletion of private DNS zone which is not managed by capz. + isManaged, err := s.isPrivateDNSManaged(ctx, s.Scope.ResourceGroup(), zoneSpec.ZoneName) + if err != nil && !azure.ResourceNotFound(err) { + return errors.Wrapf(err, "could not get private DNS zone state of %s in resource group %s", zoneSpec.ZoneName, s.Scope.ResourceGroup()) + } + if !isManaged { + log.V(1).Info("Skipping private DNS zone deletion for unmanaged private DNS zone", "private DNS", zoneSpec.ZoneName) + return nil + } // Delete the private DNS zone, which also deletes all records. log.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 @@ -140,3 +199,25 @@ func (s *Service) Delete(ctx context.Context) error { } return nil } + +// isPrivateDNSManaged returns true if the private DNS has an owned tag with the cluster name as value, +// meaning that the DNS lifecycle is managed. +func (s *Service) isPrivateDNSManaged(ctx context.Context, resourceGroup, zoneName string) (bool, error) { + zone, err := s.client.GetZone(ctx, resourceGroup, zoneName) + if err != nil { + return false, err + } + tags := converters.MapToTags(zone.Tags) + return tags.HasOwned(s.Scope.ClusterName()), nil +} + +// isVnetLinkManaged returns true if the vnet link has an owned tag with the cluster name as value, +// meaning that the vnet link lifecycle is managed. +func (s *Service) isVnetLinkManaged(ctx context.Context, resourceGroupName, zoneName, vnetLinkName string) (bool, error) { + zone, err := s.client.GetLink(ctx, resourceGroupName, zoneName, vnetLinkName) + if err != nil { + return false, err + } + tags := converters.MapToTags(zone.Tags) + return tags.HasOwned(s.Scope.ClusterName()), nil +} diff --git a/azure/services/privatedns/privatedns_test.go b/azure/services/privatedns/privatedns_test.go index f0f55885311..c5234e3235d 100644 --- a/azure/services/privatedns/privatedns_test.go +++ b/azure/services/privatedns/privatedns_test.go @@ -66,8 +66,19 @@ func TestReconcilePrivateDNS(t *testing.T) { }, }) s.ResourceGroup().AnyTimes().Return("my-rg") + s.ClusterName().AnyTimes().Return("my-cluster") + s.AdditionalTags().AnyTimes().Return(infrav1.Tags{}) s.SubscriptionID().Return("123") - m.CreateOrUpdateZone(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.PrivateZone{Location: to.StringPtr(azure.Global)}) + m.GetZone(gomockinternal.AContext(), "my-rg", "my-dns-zone"). + Return(privatedns.PrivateZone{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) + m.CreateOrUpdateZone(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.PrivateZone{ + Location: to.StringPtr(azure.Global), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + }, + }) + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link"). + Return(privatedns.VirtualNetworkLink{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) m.CreateOrUpdateLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link", privatedns.VirtualNetworkLink{ VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ VirtualNetwork: &privatedns.SubResource{ @@ -76,6 +87,9 @@ func TestReconcilePrivateDNS(t *testing.T) { 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"), + }, }) m.CreateOrUpdateRecordSet(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.A, "hostname-1", privatedns.RecordSet{ RecordSetProperties: &privatedns.RecordSetProperties{ @@ -115,8 +129,19 @@ func TestReconcilePrivateDNS(t *testing.T) { }, }) s.ResourceGroup().AnyTimes().Return("my-rg") + s.ClusterName().AnyTimes().Return("my-cluster") + s.AdditionalTags().AnyTimes().Return(infrav1.Tags{}) s.SubscriptionID().AnyTimes().Return("123") - m.CreateOrUpdateZone(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.PrivateZone{Location: to.StringPtr(azure.Global)}) + m.GetZone(gomockinternal.AContext(), "my-rg", "my-dns-zone"). + Return(privatedns.PrivateZone{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) + m.CreateOrUpdateZone(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.PrivateZone{ + Location: to.StringPtr(azure.Global), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + }, + }) + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1"). + Return(privatedns.VirtualNetworkLink{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) m.CreateOrUpdateLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1", privatedns.VirtualNetworkLink{ VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ VirtualNetwork: &privatedns.SubResource{ @@ -125,7 +150,12 @@ func TestReconcilePrivateDNS(t *testing.T) { 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"), + }, }) + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2"). + Return(privatedns.VirtualNetworkLink{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) m.CreateOrUpdateLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2", privatedns.VirtualNetworkLink{ VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ VirtualNetwork: &privatedns.SubResource{ @@ -134,6 +164,9 @@ func TestReconcilePrivateDNS(t *testing.T) { 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"), + }, }) m.CreateOrUpdateRecordSet(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.A, "hostname-1", privatedns.RecordSet{ RecordSetProperties: &privatedns.RecordSetProperties{ @@ -168,8 +201,19 @@ func TestReconcilePrivateDNS(t *testing.T) { }, }) s.ResourceGroup().AnyTimes().Return("my-rg") - s.SubscriptionID().Return("123") - m.CreateOrUpdateZone(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.PrivateZone{Location: to.StringPtr(azure.Global)}) + s.ClusterName().AnyTimes().Return("my-cluster") + s.AdditionalTags().AnyTimes().Return(infrav1.Tags{}) + s.SubscriptionID().AnyTimes().Return("123") + m.GetZone(gomockinternal.AContext(), "my-rg", "my-dns-zone"). + Return(privatedns.PrivateZone{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) + m.CreateOrUpdateZone(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.PrivateZone{ + Location: to.StringPtr(azure.Global), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + }, + }) + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link"). + Return(privatedns.VirtualNetworkLink{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) m.CreateOrUpdateLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link", privatedns.VirtualNetworkLink{ VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ VirtualNetwork: &privatedns.SubResource{ @@ -178,6 +222,9 @@ func TestReconcilePrivateDNS(t *testing.T) { 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"), + }, }) m.CreateOrUpdateRecordSet(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.AAAA, "hostname-2", privatedns.RecordSet{ RecordSetProperties: &privatedns.RecordSetProperties{ @@ -217,8 +264,19 @@ func TestReconcilePrivateDNS(t *testing.T) { }, }) s.ResourceGroup().AnyTimes().Return("my-rg") + s.ClusterName().AnyTimes().Return("my-cluster") + s.AdditionalTags().AnyTimes().Return(infrav1.Tags{}) s.SubscriptionID().AnyTimes().Return("123") - m.CreateOrUpdateZone(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.PrivateZone{Location: to.StringPtr(azure.Global)}) + m.GetZone(gomockinternal.AContext(), "my-rg", "my-dns-zone"). + Return(privatedns.PrivateZone{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) + m.CreateOrUpdateZone(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.PrivateZone{ + Location: to.StringPtr(azure.Global), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + }, + }) + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1"). + Return(privatedns.VirtualNetworkLink{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) m.CreateOrUpdateLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1", privatedns.VirtualNetworkLink{ VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ VirtualNetwork: &privatedns.SubResource{ @@ -227,7 +285,12 @@ func TestReconcilePrivateDNS(t *testing.T) { 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"), + }, }) + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2"). + Return(privatedns.VirtualNetworkLink{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) m.CreateOrUpdateLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2", privatedns.VirtualNetworkLink{ VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ VirtualNetwork: &privatedns.SubResource{ @@ -236,6 +299,9 @@ func TestReconcilePrivateDNS(t *testing.T) { 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"), + }, }) m.CreateOrUpdateRecordSet(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.AAAA, "hostname-2", privatedns.RecordSet{ RecordSetProperties: &privatedns.RecordSetProperties{ @@ -270,8 +336,19 @@ func TestReconcilePrivateDNS(t *testing.T) { }, }) s.ResourceGroup().AnyTimes().Return("my-rg") - s.SubscriptionID().Return("123") - m.CreateOrUpdateZone(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.PrivateZone{Location: to.StringPtr(azure.Global)}) + s.ClusterName().AnyTimes().Return("my-cluster") + s.AdditionalTags().AnyTimes().Return(infrav1.Tags{}) + s.SubscriptionID().AnyTimes().Return("123") + m.GetZone(gomockinternal.AContext(), "my-rg", "my-dns-zone"). + Return(privatedns.PrivateZone{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) + m.CreateOrUpdateZone(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.PrivateZone{ + Location: to.StringPtr(azure.Global), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + }, + }) + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link"). + Return(privatedns.VirtualNetworkLink{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) m.CreateOrUpdateLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link", privatedns.VirtualNetworkLink{ VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ VirtualNetwork: &privatedns.SubResource{ @@ -280,6 +357,9 @@ func TestReconcilePrivateDNS(t *testing.T) { 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"), + }, }).Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) }, }, @@ -314,8 +394,19 @@ func TestReconcilePrivateDNS(t *testing.T) { }, }) s.ResourceGroup().AnyTimes().Return("my-rg") + s.ClusterName().AnyTimes().Return("my-cluster") + s.AdditionalTags().AnyTimes().Return(infrav1.Tags{}) s.SubscriptionID().AnyTimes().Return("123") - m.CreateOrUpdateZone(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.PrivateZone{Location: to.StringPtr(azure.Global)}) + m.GetZone(gomockinternal.AContext(), "my-rg", "my-dns-zone"). + Return(privatedns.PrivateZone{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) + m.CreateOrUpdateZone(gomockinternal.AContext(), "my-rg", "my-dns-zone", privatedns.PrivateZone{ + Location: to.StringPtr(azure.Global), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + }, + }) + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1"). + Return(privatedns.VirtualNetworkLink{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) m.CreateOrUpdateLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1", privatedns.VirtualNetworkLink{ VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ VirtualNetwork: &privatedns.SubResource{ @@ -324,7 +415,12 @@ func TestReconcilePrivateDNS(t *testing.T) { 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"), + }, }) + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2"). + Return(privatedns.VirtualNetworkLink{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) m.CreateOrUpdateLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2", privatedns.VirtualNetworkLink{ VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ VirtualNetwork: &privatedns.SubResource{ @@ -333,6 +429,9 @@ func TestReconcilePrivateDNS(t *testing.T) { 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"), + }, }).Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) }, }, @@ -381,7 +480,7 @@ func TestDeletePrivateDNS(t *testing.T) { }, }, { - name: "delete the dns zone", + name: "delete the dns zone and vnet links managed by capz", expectedError: "", expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ @@ -401,10 +500,51 @@ func TestDeletePrivateDNS(t *testing.T) { }, }) s.ResourceGroup().AnyTimes().Return("my-rg") + s.ClusterName().AnyTimes().Return("my-cluster") + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link").Return(privatedns.VirtualNetworkLink{ + Name: to.StringPtr("my-link"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link") + m.GetZone(gomockinternal.AContext(), "my-rg", "my-dns-zone").Return(privatedns.PrivateZone{ + Name: to.StringPtr("my-dns-zone"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteZone(gomockinternal.AContext(), "my-rg", "my-dns-zone") }, }, + { + name: "skip unmanaged private dns zone and vnet link deletion", + expectedError: "", + expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { + s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ + ZoneName: "my-dns-zone", + Links: []azure.PrivateDNSLinkSpec{ + { + VNetName: "my-vnet", + VNetResourceGroup: "vnet-rg", + LinkName: "my-link", + }, + }, + Records: []infrav1.AddressRecord{ + { + Hostname: "hostname-1", + IP: "10.0.0.8", + }, + }, + }) + s.ResourceGroup().AnyTimes().Return("my-rg") + s.ClusterName().AnyTimes().Return("my-cluster") + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link") + m.GetZone(gomockinternal.AContext(), "my-rg", "my-dns-zone") + }, + }, { name: "delete the dns zone with multiple links", expectedError: "", @@ -436,9 +576,38 @@ func TestDeletePrivateDNS(t *testing.T) { }, }) s.ResourceGroup().AnyTimes().Return("my-rg") + s.ClusterName().AnyTimes().Return("my-cluster") + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1").Return(privatedns.VirtualNetworkLink{ + Name: to.StringPtr("my-vnet"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1") + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2").Return(privatedns.VirtualNetworkLink{ + Name: to.StringPtr("my-vnet"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2") + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-3").Return(privatedns.VirtualNetworkLink{ + Name: to.StringPtr("my-vnet"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-3") + m.GetZone(gomockinternal.AContext(), "my-rg", "my-dns-zone").Return(privatedns.PrivateZone{ + Name: to.StringPtr("my-dns-zone"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteZone(gomockinternal.AContext(), "my-rg", "my-dns-zone") }, }, @@ -463,8 +632,16 @@ func TestDeletePrivateDNS(t *testing.T) { }, }) s.ResourceGroup().AnyTimes().Return("my-rg") - m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link"). - Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) + s.ClusterName().AnyTimes().Return("my-cluster") + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link"). + Return(privatedns.VirtualNetworkLink{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) + m.GetZone(gomockinternal.AContext(), "my-rg", "my-dns-zone").Return(privatedns.PrivateZone{ + Name: to.StringPtr("my-dns-zone"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteZone(gomockinternal.AContext(), "my-rg", "my-dns-zone") }, }, @@ -499,15 +676,37 @@ func TestDeletePrivateDNS(t *testing.T) { }, }) s.ResourceGroup().AnyTimes().Return("my-rg") + s.ClusterName().AnyTimes().Return("my-cluster") + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1").Return(privatedns.VirtualNetworkLink{ + Name: to.StringPtr("my-vnet"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) 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.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2"). + Return(privatedns.VirtualNetworkLink{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-3").Return(privatedns.VirtualNetworkLink{ + Name: to.StringPtr("my-vnet"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-3") + m.GetZone(gomockinternal.AContext(), "my-rg", "my-dns-zone").Return(privatedns.PrivateZone{ + Name: to.StringPtr("my-dns-zone"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteZone(gomockinternal.AContext(), "my-rg", "my-dns-zone") }, }, { - name: "zone already deleted", + name: "zone and all vnet links already deleted", expectedError: "", expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { s.PrivateDNSSpec().Return(&azure.PrivateDNSSpec{ @@ -527,10 +726,11 @@ func TestDeletePrivateDNS(t *testing.T) { }, }) s.ResourceGroup().AnyTimes().Return("my-rg") - m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link"). - Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) - m.DeleteZone(gomockinternal.AContext(), "my-rg", "my-dns-zone"). - Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) + s.ClusterName().AnyTimes().Return("my-cluster") + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link"). + Return(privatedns.VirtualNetworkLink{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) + m.GetZone(gomockinternal.AContext(), "my-rg", "my-dns-zone"). + Return(privatedns.PrivateZone{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) }, }, { @@ -554,6 +754,14 @@ func TestDeletePrivateDNS(t *testing.T) { }, }) s.ResourceGroup().AnyTimes().Return("my-rg") + s.ClusterName().AnyTimes().Return("my-cluster") + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link").Return(privatedns.VirtualNetworkLink{ + Name: to.StringPtr("my-vnet"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link"). Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) }, @@ -589,7 +797,22 @@ func TestDeletePrivateDNS(t *testing.T) { }, }) s.ResourceGroup().AnyTimes().Return("my-rg") + s.ClusterName().AnyTimes().Return("my-cluster") + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1").Return(privatedns.VirtualNetworkLink{ + Name: to.StringPtr("my-vnet"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1") + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2").Return(privatedns.VirtualNetworkLink{ + Name: to.StringPtr("my-vnet"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2"). Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) }, @@ -615,7 +838,22 @@ func TestDeletePrivateDNS(t *testing.T) { }, }) s.ResourceGroup().AnyTimes().Return("my-rg") + s.ClusterName().AnyTimes().Return("my-cluster") + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link").Return(privatedns.VirtualNetworkLink{ + Name: to.StringPtr("my-vnet"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link") + m.GetZone(gomockinternal.AContext(), "my-rg", "my-dns-zone").Return(privatedns.PrivateZone{ + Name: to.StringPtr("my-dns-zone"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteZone(gomockinternal.AContext(), "my-rg", "my-dns-zone"). Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) }, @@ -651,9 +889,38 @@ func TestDeletePrivateDNS(t *testing.T) { }, }) s.ResourceGroup().AnyTimes().Return("my-rg") + s.ClusterName().AnyTimes().Return("my-cluster") + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1").Return(privatedns.VirtualNetworkLink{ + Name: to.StringPtr("my-vnet"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-1") + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2").Return(privatedns.VirtualNetworkLink{ + Name: to.StringPtr("my-vnet"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-2") + m.GetLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-3").Return(privatedns.VirtualNetworkLink{ + Name: to.StringPtr("my-vnet"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteLink(gomockinternal.AContext(), "my-rg", "my-dns-zone", "my-link-3") + m.GetZone(gomockinternal.AContext(), "my-rg", "my-dns-zone").Return(privatedns.PrivateZone{ + Name: to.StringPtr("my-dns-zone"), + Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_my-cluster": to.StringPtr("owned"), + "foo": to.StringPtr("bar"), + }, + }, nil) m.DeleteZone(gomockinternal.AContext(), "my-rg", "my-dns-zone"). Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) }, diff --git a/docs/book/src/topics/custom-dns.md b/docs/book/src/topics/custom-dns.md index ee82149e3dd..2e7bc2291a4 100644 --- a/docs/book/src/topics/custom-dns.md +++ b/docs/book/src/topics/custom-dns.md @@ -35,3 +35,18 @@ spec: resourceGroup: cluster-example ``` +# Manage DNS Via CAPZ Tool + +Private DNS when created by CAPZ can be managed by CAPZ tool itself automatically. To give the flexibility to have BYO +as well as managed DNS zone, an enhancement is made that causes all the managed zones created in the CAPZ version before +the enhancement changes to be treated as unmanaged. The enhancement is captured in PR +[1791](https://github.com/kubernetes-sigs/cluster-api-provider-azure/pull/1791) + +To manage the private DNS via CAPZ please tag it manually from azure portal. + +Steps to tag: + +- Go to azure portal and search for `Private DNS zones`. +- Select the DNS zone that you want to be managed. +- Go to `Tags` section and add key as `sigs.k8s.io_cluster-api-provider-azure_cluster_` and value as +`owned`. (Note: clustername is the name of the cluster that you created) \ No newline at end of file