diff --git a/api/v1beta1/conditions_consts.go b/api/v1beta1/conditions_consts.go index 775bc5a81b1..a8afabe78cc 100644 --- a/api/v1beta1/conditions_consts.go +++ b/api/v1beta1/conditions_consts.go @@ -104,8 +104,12 @@ const ( SubnetsReadyCondition clusterv1.ConditionType = "SubnetsReady" // LoadBalancersReadyCondition means the load balancers exist and are ready to be used. LoadBalancersReadyCondition clusterv1.ConditionType = "LoadBalancersReady" - // PrivateDNSReadyCondition means the private DNS exists and is ready to be used. - PrivateDNSReadyCondition clusterv1.ConditionType = "PrivateDNSReady" + // PrivateDNSZoneReadyCondition means the private DNS zone exists and is ready to be used. + PrivateDNSZoneReadyCondition clusterv1.ConditionType = "PrivateDNSZoneReady" + // PrivateDNSLinkReadyCondition means the private DNS links exist and are ready to be used. + PrivateDNSLinkReadyCondition clusterv1.ConditionType = "PrivateDNSLinkReady" + // PrivateDNSRecordReadyCondition means the private DNS records exist and are ready to be used. + PrivateDNSRecordReadyCondition clusterv1.ConditionType = "PrivateDNSRecordReady" // BastionHostReadyCondition means the bastion host exists and is ready to be used. BastionHostReadyCondition clusterv1.ConditionType = "BastionHostReady" // InboundNATRulesReadyCondition means the inbound NAT rules exist and are ready to be used. diff --git a/azure/scope/cluster.go b/azure/scope/cluster.go index c596b75de45..df278356476 100644 --- a/azure/scope/cluster.go +++ b/azure/scope/cluster.go @@ -34,6 +34,7 @@ import ( "sigs.k8s.io/cluster-api-provider-azure/azure/services/groups" "sigs.k8s.io/cluster-api-provider-azure/azure/services/loadbalancers" "sigs.k8s.io/cluster-api-provider-azure/azure/services/natgateways" + "sigs.k8s.io/cluster-api-provider-azure/azure/services/privatedns" "sigs.k8s.io/cluster-api-provider-azure/azure/services/routetables" "sigs.k8s.io/cluster-api-provider-azure/azure/services/securitygroups" "sigs.k8s.io/cluster-api-provider-azure/azure/services/subnets" @@ -387,35 +388,53 @@ func (s *ClusterScope) VNetSpec() azure.ResourceSpecGetter { } // PrivateDNSSpec returns the private dns zone spec. -func (s *ClusterScope) PrivateDNSSpec() *azure.PrivateDNSSpec { - var specs *azure.PrivateDNSSpec +func (s *ClusterScope) PrivateDNSSpec() (zoneSpec azure.ResourceSpecGetter, linkSpec, recordSpec []azure.ResourceSpecGetter) { if s.IsAPIServerPrivate() { - links := make([]azure.PrivateDNSLinkSpec, 1+len(s.Vnet().Peerings)) - links[0] = azure.PrivateDNSLinkSpec{ - VNetName: s.Vnet().Name, + zone := privatedns.ZoneSpec{ + Name: s.GetPrivateDNSZoneName(), + ResourceGroup: s.ResourceGroup(), + ClusterName: s.ClusterName(), + AdditionalTags: s.AdditionalTags(), + } + + links := make([]azure.ResourceSpecGetter, 1+len(s.Vnet().Peerings)) + links[0] = privatedns.LinkSpec{ + Name: azure.GenerateVNetLinkName(s.Vnet().Name), + ZoneName: s.GetPrivateDNSZoneName(), + SubscriptionID: s.SubscriptionID(), VNetResourceGroup: s.Vnet().ResourceGroup, - LinkName: azure.GenerateVNetLinkName(s.Vnet().Name), + VNetName: s.Vnet().Name, + ResourceGroup: s.ResourceGroup(), + ClusterName: s.ClusterName(), + AdditionalTags: s.AdditionalTags(), } for i, peering := range s.Vnet().Peerings { - links[i+1] = azure.PrivateDNSLinkSpec{ - VNetName: peering.RemoteVnetName, + links[i+1] = privatedns.LinkSpec{ + Name: azure.GenerateVNetLinkName(peering.RemoteVnetName), + ZoneName: s.GetPrivateDNSZoneName(), + SubscriptionID: s.SubscriptionID(), VNetResourceGroup: peering.ResourceGroup, - LinkName: azure.GenerateVNetLinkName(peering.RemoteVnetName), + VNetName: peering.RemoteVnetName, + ResourceGroup: s.ResourceGroup(), + ClusterName: s.ClusterName(), + AdditionalTags: s.AdditionalTags(), } } - specs = &azure.PrivateDNSSpec{ - ZoneName: s.GetPrivateDNSZoneName(), - Links: links, - Records: []infrav1.AddressRecord{ - { - Hostname: azure.PrivateAPIServerHostname, - IP: s.APIServerPrivateIP(), - }, + + records := make([]azure.ResourceSpecGetter, 1) + records[0] = privatedns.RecordSpec{ + Record: infrav1.AddressRecord{ + Hostname: azure.PrivateAPIServerHostname, + IP: s.APIServerPrivateIP(), }, + ZoneName: s.GetPrivateDNSZoneName(), + ResourceGroup: s.ResourceGroup(), } + + return zone, links, records } - return specs + return nil, nil, nil } // IsAzureBastionEnabled returns true if the azure bastion is enabled. @@ -700,6 +719,9 @@ func (s *ClusterScope) PatchObject(ctx context.Context) error { infrav1.VNetReadyCondition, infrav1.SubnetsReadyCondition, infrav1.SecurityGroupsReadyCondition, + infrav1.PrivateDNSZoneReadyCondition, + infrav1.PrivateDNSLinkReadyCondition, + infrav1.PrivateDNSRecordReadyCondition, }}) } diff --git a/azure/services/privatedns/client.go b/azure/services/privatedns/client.go deleted file mode 100644 index 87f0a7abb36..00000000000 --- a/azure/services/privatedns/client.go +++ /dev/null @@ -1,183 +0,0 @@ -/* -Copyright 2019 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" - "sigs.k8s.io/cluster-api-provider-azure/azure" - "sigs.k8s.io/cluster-api-provider-azure/util/tele" -) - -// 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 - DeleteRecordSet(context.Context, string, string, privatedns.RecordType, string) error -} - -// AzureClient contains the Azure go-sdk Client. -type azureClient struct { - privatezones privatedns.PrivateZonesClient - vnetlinks privatedns.VirtualNetworkLinksClient - recordsets privatedns.RecordSetsClient -} - -var _ client = (*azureClient)(nil) - -// newClient creates a new VM client from subscription ID. -func newClient(auth azure.Authorizer) *azureClient { - c := newPrivateZonesClient(auth.SubscriptionID(), auth.BaseURI(), auth.Authorizer()) - v := newVirtualNetworkLinksClient(auth.SubscriptionID(), auth.BaseURI(), auth.Authorizer()) - r := newRecordSetsClient(auth.SubscriptionID(), auth.BaseURI(), auth.Authorizer()) - return &azureClient{c, v, r} -} - -// newPrivateZonesClient creates a new private zones client from subscription ID. -func newPrivateZonesClient(subscriptionID string, baseURI string, authorizer autorest.Authorizer) privatedns.PrivateZonesClient { - zonesClient := privatedns.NewPrivateZonesClientWithBaseURI(baseURI, subscriptionID) - azure.SetAutoRestClientDefaults(&zonesClient.Client, authorizer) - return zonesClient -} - -// newVirtualNetworkLinksClient creates a new virtual networks link client from subscription ID. -func newVirtualNetworkLinksClient(subscriptionID string, baseURI string, authorizer autorest.Authorizer) privatedns.VirtualNetworkLinksClient { - linksClient := privatedns.NewVirtualNetworkLinksClientWithBaseURI(baseURI, subscriptionID) - azure.SetAutoRestClientDefaults(&linksClient.Client, authorizer) - return linksClient -} - -// newRecordSetsClient creates a new record sets client from subscription ID. -func newRecordSetsClient(subscriptionID string, baseURI string, authorizer autorest.Authorizer) privatedns.RecordSetsClient { - recordsClient := privatedns.NewRecordSetsClientWithBaseURI(baseURI, subscriptionID) - azure.SetAutoRestClientDefaults(&recordsClient.Client, authorizer) - 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 - } - err = future.WaitForCompletionRef(ctx, ac.privatezones.Client) - if err != nil { - return err - } - _, err = future.Result(ac.privatezones) - return err -} - -// DeleteZone deletes the private zone. -func (ac *azureClient) DeleteZone(ctx context.Context, resourceGroupName, name string) error { - ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.AzureClient.DeleteZone") - defer done() - - future, err := ac.privatezones.Delete(ctx, resourceGroupName, name, "") - if err != nil { - return err - } - err = future.WaitForCompletionRef(ctx, ac.privatezones.Client) - if err != nil { - return err - } - _, err = future.Result(ac.privatezones) - 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") - defer done() - - future, err := ac.vnetlinks.CreateOrUpdate(ctx, resourceGroupName, privateZoneName, name, link, "", "") - if err != nil { - return err - } - err = future.WaitForCompletionRef(ctx, ac.vnetlinks.Client) - if err != nil { - return err - } - _, err = future.Result(ac.vnetlinks) - return err -} - -// DeleteLink deletes a virtual network link to the specified Private DNS zone. -func (ac *azureClient) DeleteLink(ctx context.Context, resourceGroupName, privateZoneName, name string) error { - ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.AzureClient.DeleteLink") - defer done() - - future, err := ac.vnetlinks.Delete(ctx, resourceGroupName, privateZoneName, name, "") - if err != nil { - return err - } - err = future.WaitForCompletionRef(ctx, ac.vnetlinks.Client) - if err != nil { - return err - } - _, err = future.Result(ac.vnetlinks) - return err -} - -// CreateOrUpdateRecordSet creates or updates a record set within the specified Private DNS zone. -func (ac *azureClient) CreateOrUpdateRecordSet(ctx context.Context, resourceGroupName string, privateZoneName string, recordType privatedns.RecordType, name string, set privatedns.RecordSet) error { - ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.AzureClient.CreateOrUpdateRecordSet") - defer done() - - _, err := ac.recordsets.CreateOrUpdate(ctx, resourceGroupName, privateZoneName, recordType, name, set, "", "") - return err -} - -// DeleteRecordSet deletes a record set within the specified Private DNS zone. -func (ac *azureClient) DeleteRecordSet(ctx context.Context, resourceGroupName string, privateZoneName string, recordType privatedns.RecordType, name string) error { - ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.AzureClient.DeleteRecordSet") - defer done() - - _, err := ac.recordsets.Delete(ctx, resourceGroupName, privateZoneName, recordType, name, "") - return err -} diff --git a/azure/services/privatedns/link_client.go b/azure/services/privatedns/link_client.go new file mode 100644 index 00000000000..075b56b7525 --- /dev/null +++ b/azure/services/privatedns/link_client.go @@ -0,0 +1,158 @@ +/* +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" + "encoding/json" + + "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns" + azureautorest "github.com/Azure/go-autorest/autorest/azure" + "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/reconciler" + "sigs.k8s.io/cluster-api-provider-azure/util/tele" +) + +// azureVirtualNetworkLinksClient contains the Azure go-sdk Client for virtual network links. +type azureVirtualNetworkLinksClient struct { + vnetlinks privatedns.VirtualNetworkLinksClient +} + +// newVirtualNetworkLinksClient creates a new virtual network link client. +func newVirtualNetworkLinksClient(auth azure.Authorizer) *azureVirtualNetworkLinksClient { + linksClient := privatedns.NewVirtualNetworkLinksClientWithBaseURI(auth.BaseURI(), auth.SubscriptionID()) + azure.SetAutoRestClientDefaults(&linksClient.Client, auth.Authorizer()) + return &azureVirtualNetworkLinksClient{ + vnetlinks: linksClient, + } +} + +// CreateOrUpdateAsync creates or updates a virtual network link asynchronously. +// It sends a PUT request to Azure and if accepted without error, the func will return a Future which can be used to track the ongoing +// progress of the operation. +func (avc *azureVirtualNetworkLinksClient) CreateOrUpdateAsync(ctx context.Context, spec azure.ResourceSpecGetter, parameters interface{}) (result interface{}, future azureautorest.FutureAPI, err error) { + ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.azureVirtualNetworkLinksClient.CreateOrUpdateAsync") + defer done() + + link, ok := parameters.(privatedns.VirtualNetworkLink) + if !ok { + return nil, nil, errors.Errorf("%T is not a privatedns.VirtualNetworkLink", parameters) + } + + createFuture, err := avc.vnetlinks.CreateOrUpdate(ctx, spec.ResourceGroupName(), spec.OwnerResourceName(), spec.ResourceName(), link, "", "") + if err != nil { + return nil, nil, err + } + + ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureCallTimeout) + defer cancel() + + err = createFuture.WaitForCompletionRef(ctx, avc.vnetlinks.Client) + if err != nil { + // if an error occurs, return the future. + // this means the long-running operation didn't finish in the specified timeout. + return nil, &createFuture, err + } + result, err = createFuture.Result(avc.vnetlinks) + // if the operation completed, return a nil future + return result, nil, err +} + +// Get gets the specified virtual network link. +func (avc *azureVirtualNetworkLinksClient) Get(ctx context.Context, spec azure.ResourceSpecGetter) (result interface{}, err error) { + ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.azureVirtualNetworkLinksClient.Get") + defer done() + link, err := avc.vnetlinks.Get(ctx, spec.ResourceGroupName(), spec.OwnerResourceName(), spec.ResourceName()) + if err != nil { + return privatedns.VirtualNetworkLink{}, err + } + return link, nil +} + +// DeleteAsync deletes a virtual network link asynchronously. DeleteAsync sends a DELETE +// request to Azure and if accepted without error, the func will return a Future which can be used to track the ongoing +// progress of the operation. +func (avc *azureVirtualNetworkLinksClient) DeleteAsync(ctx context.Context, spec azure.ResourceSpecGetter) (future azureautorest.FutureAPI, err error) { + ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.azureVirtualNetworkLinksClient.DeleteAsync") + defer done() + + deleteFuture, err := avc.vnetlinks.Delete(ctx, spec.ResourceGroupName(), spec.OwnerResourceName(), spec.ResourceName(), "") + if err != nil { + return nil, err + } + + ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureCallTimeout) + defer cancel() + + err = deleteFuture.WaitForCompletionRef(ctx, avc.vnetlinks.Client) + if err != nil { + // if an error occurs, return the future. + // this means the long-running operation didn't finish in the specified timeout. + return &deleteFuture, err + } + _, err = deleteFuture.Result(avc.vnetlinks) + // if the operation completed, return a nil future. + return nil, err +} + +// IsDone returns true if the long-running operation has completed. +func (avc *azureVirtualNetworkLinksClient) IsDone(ctx context.Context, future azureautorest.FutureAPI) (isDone bool, err error) { + ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.azureVirtualNetworkLinksClient.IsDone") + defer done() + + isDone, err = future.DoneWithContext(ctx, avc.vnetlinks) + if err != nil { + return false, errors.Wrap(err, "failed checking if the operation was complete") + } + + return isDone, nil +} + +// Result fetches the result of a long-running operation future. +func (avc *azureVirtualNetworkLinksClient) Result(ctx context.Context, future azureautorest.FutureAPI, futureType string) (result interface{}, err error) { + _, _, done := tele.StartSpanWithLogger(ctx, "privatedns.azureVirtualNetworkLinksClient.Result") + defer done() + + if future == nil { + return nil, errors.Errorf("cannot get result from nil future") + } + + switch futureType { + case infrav1.PutFuture: + // Marshal and Unmarshal the future to put it into the correct future type so we can access the Result function. + // Unfortunately the FutureAPI can't be casted directly to VirtualNetworkLinksCreateOrUpdateFuture because it is a azureautorest.Future, which doesn't implement the Result function. See PR #1686 for discussion on alternatives. + // It was converted back to a generic azureautorest.Future from the CAPZ infrav1.Future type stored in Status: https://github.com/kubernetes-sigs/cluster-api-provider-azure/blob/main/azure/converters/futures.go#L49. + var createFuture *privatedns.VirtualNetworkLinksCreateOrUpdateFuture + jsonData, err := future.MarshalJSON() + if err != nil { + return nil, errors.Wrap(err, "failed to marshal future") + } + if err := json.Unmarshal(jsonData, &createFuture); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal future data") + } + return createFuture.Result(avc.vnetlinks) + + case infrav1.DeleteFuture: + // Delete does not return a result private dns virtual network link. + return nil, nil + + default: + return nil, errors.Errorf("unknown future type %q", futureType) + } +} diff --git a/azure/services/privatedns/link_reconciler.go b/azure/services/privatedns/link_reconciler.go new file mode 100644 index 00000000000..8fc31c2a848 --- /dev/null +++ b/azure/services/privatedns/link_reconciler.go @@ -0,0 +1,103 @@ +/* +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/pkg/errors" + "sigs.k8s.io/cluster-api-provider-azure/azure" + "sigs.k8s.io/cluster-api-provider-azure/util/tele" +) + +func (s *Service) reconcileLinks(ctx context.Context, links []azure.ResourceSpecGetter) (managed bool, err error) { + ctx, log, done := tele.StartSpanWithLogger(ctx, "privatedns.Service.reconcileLinks") + defer done() + + var resErr error + + // We go through the list of links to reconcile each one, independently of the result of the previous one. + // If multiple errors occur, we return the most pressing one. + // Order of precedence (highest -> lowest) is: error that is not an operationNotDoneError (i.e. error creating) -> operationNotDoneError (i.e. creating in progress) -> no error (i.e. created) + for _, linkSpec := range links { + isLinkManaged, err := s.isVnetLinkManaged(ctx, linkSpec) + if err != nil { + if azure.ResourceNotFound(err) { + isLinkManaged = true + } else { + return managed, err + } + } + + if !isLinkManaged { + log.V(2).Info("Skipping vnet link reconciliation for unmanaged vnet link", "vnet link", + linkSpec.ResourceName(), "private dns zone", linkSpec.OwnerResourceName()) + continue + } + + // we consider VnetLinks as managed if at least of the links is managed. + managed = true + if _, err := s.vnetLinkReconciler.CreateResource(ctx, linkSpec, serviceName); err != nil { + if !azure.IsOperationNotDoneError(err) || resErr == nil { + resErr = err + } + } + } + + return managed, resErr +} + +func (s *Service) deleteLinks(ctx context.Context, links []azure.ResourceSpecGetter) (managed bool, err error) { + ctx, log, done := tele.StartSpanWithLogger(ctx, "privatedns.Service.deleteLinks") + defer done() + + var resErr error + + // We go through the list of links to delete each one, independently of the result of the previous one. + // If multiple errors occur, we return the most pressing one. + // Order of precedence (highest -> lowest) is: error that is not an operationNotDoneError (i.e. error creating) -> operationNotDoneError (i.e. creating in progress) -> no error (i.e. created) + for _, linkSpec := range links { + // If the virtual network link is not managed by capz, skip its reconciliation + isVnetLinkManaged, err := s.isVnetLinkManaged(ctx, linkSpec) + if err != nil { + if azure.ResourceNotFound(err) { + // already deleted or doesn't exist, cleanup status and return. + s.Scope.DeleteLongRunningOperationState(linkSpec.ResourceName(), serviceName) + continue + } + return managed, errors.Wrapf(err, "could not get vnet link state of %s in resource group %s", + linkSpec.OwnerResourceName(), linkSpec.ResourceGroupName()) + } + + if !isVnetLinkManaged { + log.V(2).Info("Skipping vnet link deletion for unmanaged vnet link", "vnet link", + linkSpec.ResourceName(), "private dns zone", linkSpec.OwnerResourceName()) + continue + } + + // if we reach here, it means that this vnet link is managed by capz. + managed = true + + if err := s.vnetLinkReconciler.DeleteResource(ctx, linkSpec, serviceName); err != nil { + if !azure.IsOperationNotDoneError(err) || resErr == nil { + resErr = err + } + } + } + + return managed, resErr +} diff --git a/azure/services/privatedns/link_spec.go b/azure/services/privatedns/link_spec.go new file mode 100644 index 00000000000..36047803bd7 --- /dev/null +++ b/azure/services/privatedns/link_spec.go @@ -0,0 +1,79 @@ +/* +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 ( + "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" +) + +// 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 +} + +// ResourceName returns the name of the virtual network link. +func (s LinkSpec) ResourceName() string { + return s.Name +} + +// OwnerResourceName returns the zone name of the virtual network link. +func (s LinkSpec) OwnerResourceName() string { + return s.ZoneName +} + +// ResourceGroupName returns the name of the resource group of the virtual network link. +func (s LinkSpec) ResourceGroupName() string { + return s.ResourceGroup +} + +// Parameters returns the parameters for the virtual network link. +func (s LinkSpec) Parameters(existing interface{}) (params interface{}, err error) { + if existing != nil { + _, ok := existing.(privatedns.VirtualNetworkLink) + if !ok { + return nil, errors.Errorf("%T is not a privatedns.VirtualNetworkLink", existing) + } + return nil, nil + } + + return privatedns.VirtualNetworkLink{ + VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ + VirtualNetwork: &privatedns.SubResource{ + ID: to.StringPtr(azure.VNetID(s.SubscriptionID, s.VNetResourceGroup, s.VNetName)), + }, + RegistrationEnabled: to.BoolPtr(false), + }, + Location: to.StringPtr(azure.Global), + Tags: converters.TagsToMap(infrav1.Build(infrav1.BuildParams{ + ClusterName: s.ClusterName, + Lifecycle: infrav1.ResourceLifecycleOwned, + Additional: s.AdditionalTags, + })), + }, nil +} diff --git a/azure/services/privatedns/link_spec_test.go b/azure/services/privatedns/link_spec_test.go new file mode 100644 index 00000000000..6055e393b53 --- /dev/null +++ b/azure/services/privatedns/link_spec_test.go @@ -0,0 +1,127 @@ +/* +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" + "sigs.k8s.io/cluster-api-provider-azure/azure" +) + +var ( + 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, + } +) + +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(BeNil()) + }, + }, + { + 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) + } + }) + } +} diff --git a/azure/services/privatedns/mock_privatedns/client_mock.go b/azure/services/privatedns/mock_privatedns/client_mock.go deleted file mode 100644 index cf9d71ec70c..00000000000 --- a/azure/services/privatedns/mock_privatedns/client_mock.go +++ /dev/null @@ -1,166 +0,0 @@ -/* -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_privatedns is a generated GoMock package. -package mock_privatedns - -import ( - context "context" - reflect "reflect" - - privatedns "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns" - 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 -} - -// CreateOrUpdateLink mocks base method. -func (m *Mockclient) CreateOrUpdateLink(arg0 context.Context, arg1, arg2, arg3 string, arg4 privatedns.VirtualNetworkLink) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateOrUpdateLink", arg0, arg1, arg2, arg3, arg4) - ret0, _ := ret[0].(error) - return ret0 -} - -// CreateOrUpdateLink indicates an expected call of CreateOrUpdateLink. -func (mr *MockclientMockRecorder) CreateOrUpdateLink(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateLink", reflect.TypeOf((*Mockclient)(nil).CreateOrUpdateLink), arg0, arg1, arg2, arg3, arg4) -} - -// CreateOrUpdateRecordSet mocks base method. -func (m *Mockclient) CreateOrUpdateRecordSet(arg0 context.Context, arg1, arg2 string, arg3 privatedns.RecordType, arg4 string, arg5 privatedns.RecordSet) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateOrUpdateRecordSet", arg0, arg1, arg2, arg3, arg4, arg5) - ret0, _ := ret[0].(error) - return ret0 -} - -// CreateOrUpdateRecordSet indicates an expected call of CreateOrUpdateRecordSet. -func (mr *MockclientMockRecorder) CreateOrUpdateRecordSet(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateRecordSet", reflect.TypeOf((*Mockclient)(nil).CreateOrUpdateRecordSet), arg0, arg1, arg2, arg3, arg4, arg5) -} - -// CreateOrUpdateZone mocks base method. -func (m *Mockclient) CreateOrUpdateZone(arg0 context.Context, arg1, arg2 string, arg3 privatedns.PrivateZone) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateOrUpdateZone", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(error) - return ret0 -} - -// CreateOrUpdateZone indicates an expected call of CreateOrUpdateZone. -func (mr *MockclientMockRecorder) CreateOrUpdateZone(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateZone", reflect.TypeOf((*Mockclient)(nil).CreateOrUpdateZone), arg0, arg1, arg2, arg3) -} - -// DeleteLink mocks base method. -func (m *Mockclient) DeleteLink(arg0 context.Context, arg1, arg2, arg3 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteLink", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteLink indicates an expected call of DeleteLink. -func (mr *MockclientMockRecorder) DeleteLink(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLink", reflect.TypeOf((*Mockclient)(nil).DeleteLink), arg0, arg1, arg2, arg3) -} - -// DeleteRecordSet mocks base method. -func (m *Mockclient) DeleteRecordSet(arg0 context.Context, arg1, arg2 string, arg3 privatedns.RecordType, arg4 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteRecordSet", arg0, arg1, arg2, arg3, arg4) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteRecordSet indicates an expected call of DeleteRecordSet. -func (mr *MockclientMockRecorder) DeleteRecordSet(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRecordSet", reflect.TypeOf((*Mockclient)(nil).DeleteRecordSet), arg0, arg1, arg2, arg3, arg4) -} - -// DeleteZone mocks base method. -func (m *Mockclient) DeleteZone(arg0 context.Context, arg1, arg2 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteZone", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteZone indicates an expected call of DeleteZone. -func (mr *MockclientMockRecorder) DeleteZone(arg0, arg1, arg2 interface{}) *gomock.Call { - 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/mock_privatedns/doc.go b/azure/services/privatedns/mock_privatedns/doc.go index d62815a20b1..2f8c258f531 100644 --- a/azure/services/privatedns/mock_privatedns/doc.go +++ b/azure/services/privatedns/mock_privatedns/doc.go @@ -15,8 +15,6 @@ limitations under the License. */ // Run go generate to regenerate this mock. -//go:generate ../../../../hack/tools/bin/mockgen -destination client_mock.go -package mock_privatedns -source ../client.go Client //go:generate ../../../../hack/tools/bin/mockgen -destination privatedns_mock.go -package mock_privatedns -source ../privatedns.go Scope -//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 privatedns_mock.go > _privatedns_mock.go && mv _privatedns_mock.go privatedns_mock.go" package mock_privatedns //nolint diff --git a/azure/services/privatedns/mock_privatedns/privatedns_mock.go b/azure/services/privatedns/mock_privatedns/privatedns_mock.go index b071e4e5e1f..e6e48822ad5 100644 --- a/azure/services/privatedns/mock_privatedns/privatedns_mock.go +++ b/azure/services/privatedns/mock_privatedns/privatedns_mock.go @@ -27,6 +27,7 @@ import ( 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" + v1beta10 "sigs.k8s.io/cluster-api/api/v1beta1" ) // MockScope is a mock of Scope interface. @@ -178,6 +179,18 @@ func (mr *MockScopeMockRecorder) ClusterName() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterName", reflect.TypeOf((*MockScope)(nil).ClusterName)) } +// DeleteLongRunningOperationState mocks base method. +func (m *MockScope) DeleteLongRunningOperationState(arg0, arg1 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "DeleteLongRunningOperationState", arg0, arg1) +} + +// DeleteLongRunningOperationState indicates an expected call of DeleteLongRunningOperationState. +func (mr *MockScopeMockRecorder) DeleteLongRunningOperationState(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLongRunningOperationState", reflect.TypeOf((*MockScope)(nil).DeleteLongRunningOperationState), arg0, arg1) +} + // FailureDomains mocks base method. func (m *MockScope) FailureDomains() []string { m.ctrl.T.Helper() @@ -192,6 +205,20 @@ func (mr *MockScopeMockRecorder) FailureDomains() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FailureDomains", reflect.TypeOf((*MockScope)(nil).FailureDomains)) } +// GetLongRunningOperationState mocks base method. +func (m *MockScope) GetLongRunningOperationState(arg0, arg1 string) *v1beta1.Future { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLongRunningOperationState", arg0, arg1) + ret0, _ := ret[0].(*v1beta1.Future) + return ret0 +} + +// GetLongRunningOperationState indicates an expected call of GetLongRunningOperationState. +func (mr *MockScopeMockRecorder) GetLongRunningOperationState(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLongRunningOperationState", reflect.TypeOf((*MockScope)(nil).GetLongRunningOperationState), arg0, arg1) +} + // HashKey mocks base method. func (m *MockScope) HashKey() string { m.ctrl.T.Helper() @@ -221,11 +248,13 @@ func (mr *MockScopeMockRecorder) Location() *gomock.Call { } // PrivateDNSSpec mocks base method. -func (m *MockScope) PrivateDNSSpec() *azure.PrivateDNSSpec { +func (m *MockScope) PrivateDNSSpec() (azure.ResourceSpecGetter, []azure.ResourceSpecGetter, []azure.ResourceSpecGetter) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PrivateDNSSpec") - ret0, _ := ret[0].(*azure.PrivateDNSSpec) - return ret0 + ret0, _ := ret[0].(azure.ResourceSpecGetter) + ret1, _ := ret[1].([]azure.ResourceSpecGetter) + ret2, _ := ret[2].([]azure.ResourceSpecGetter) + return ret0, ret1, ret2 } // PrivateDNSSpec indicates an expected call of PrivateDNSSpec. @@ -248,6 +277,18 @@ func (mr *MockScopeMockRecorder) ResourceGroup() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResourceGroup", reflect.TypeOf((*MockScope)(nil).ResourceGroup)) } +// SetLongRunningOperationState mocks base method. +func (m *MockScope) SetLongRunningOperationState(arg0 *v1beta1.Future) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetLongRunningOperationState", arg0) +} + +// SetLongRunningOperationState indicates an expected call of SetLongRunningOperationState. +func (mr *MockScopeMockRecorder) SetLongRunningOperationState(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLongRunningOperationState", reflect.TypeOf((*MockScope)(nil).SetLongRunningOperationState), arg0) +} + // SubscriptionID mocks base method. func (m *MockScope) SubscriptionID() string { m.ctrl.T.Helper() @@ -275,3 +316,39 @@ func (mr *MockScopeMockRecorder) TenantID() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TenantID", reflect.TypeOf((*MockScope)(nil).TenantID)) } + +// UpdateDeleteStatus mocks base method. +func (m *MockScope) UpdateDeleteStatus(arg0 v1beta10.ConditionType, arg1 string, arg2 error) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UpdateDeleteStatus", arg0, arg1, arg2) +} + +// UpdateDeleteStatus indicates an expected call of UpdateDeleteStatus. +func (mr *MockScopeMockRecorder) UpdateDeleteStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeleteStatus", reflect.TypeOf((*MockScope)(nil).UpdateDeleteStatus), arg0, arg1, arg2) +} + +// UpdatePatchStatus mocks base method. +func (m *MockScope) UpdatePatchStatus(arg0 v1beta10.ConditionType, arg1 string, arg2 error) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UpdatePatchStatus", arg0, arg1, arg2) +} + +// UpdatePatchStatus indicates an expected call of UpdatePatchStatus. +func (mr *MockScopeMockRecorder) UpdatePatchStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePatchStatus", reflect.TypeOf((*MockScope)(nil).UpdatePatchStatus), arg0, arg1, arg2) +} + +// UpdatePutStatus mocks base method. +func (m *MockScope) UpdatePutStatus(arg0 v1beta10.ConditionType, arg1 string, arg2 error) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UpdatePutStatus", arg0, arg1, arg2) +} + +// UpdatePutStatus indicates an expected call of UpdatePutStatus. +func (mr *MockScopeMockRecorder) UpdatePutStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePutStatus", reflect.TypeOf((*MockScope)(nil).UpdatePutStatus), arg0, arg1, arg2) +} diff --git a/azure/services/privatedns/privatedns.go b/azure/services/privatedns/privatedns.go index b9fdf6066b2..7c6df272bc6 100644 --- a/azure/services/privatedns/privatedns.go +++ b/azure/services/privatedns/privatedns.go @@ -20,11 +20,12 @@ 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/azure/services/async" + "sigs.k8s.io/cluster-api-provider-azure/util/reconciler" "sigs.k8s.io/cluster-api-provider-azure/util/tele" ) @@ -33,20 +34,33 @@ const serviceName = "privatedns" // Scope defines the scope interface for a private dns service. type Scope interface { azure.ClusterDescriber - PrivateDNSSpec() *azure.PrivateDNSSpec + azure.Authorizer + azure.AsyncStatusUpdater + PrivateDNSSpec() (zoneSpec azure.ResourceSpecGetter, linksSpec, recordsSpec []azure.ResourceSpecGetter) } // Service provides operations on Azure resources. type Service struct { - Scope Scope - client + Scope Scope + zoneGetter async.Getter + vnetLinkGetter async.Getter + zoneReconciler async.Reconciler + vnetLinkReconciler async.Reconciler + recordReconciler async.Reconciler } // New creates a new private dns service. func New(scope Scope) *Service { + zoneClient := newPrivateZonesClient(scope) + vnetLinkClient := newVirtualNetworkLinksClient(scope) + recordSetsClient := newRecordSetsClient(scope) return &Service{ - Scope: scope, - client: newClient(scope), + Scope: scope, + zoneGetter: zoneClient, + vnetLinkGetter: vnetLinkClient, + zoneReconciler: async.New(scope, zoneClient, zoneClient), + vnetLinkReconciler: async.New(scope, vnetLinkClient, vnetLinkClient), + recordReconciler: async.New(scope, recordSetsClient, recordSetsClient), } } @@ -57,178 +71,102 @@ func (s *Service) Name() string { // Reconcile creates or updates the private zone, links it to the vnet, and creates DNS records. func (s *Service) Reconcile(ctx context.Context) error { - ctx, log, done := tele.StartSpanWithLogger(ctx, "privatedns.Service.Reconcile") + ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.Service.Reconcile") defer done() - 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) - 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{ - VirtualNetworkLinkProperties: &privatedns.VirtualNetworkLinkProperties{ - VirtualNetwork: &privatedns.SubResource{ - ID: to.StringPtr(azure.VNetID(s.Scope.SubscriptionID(), linkSpec.VNetResourceGroup, linkSpec.VNetName)), - }, - 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 { - return errors.Wrapf(err, "failed to create virtual network link %s", linkSpec.LinkName) - } - 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) - set := privatedns.RecordSet{ - RecordSetProperties: &privatedns.RecordSetProperties{ - TTL: to.Int64Ptr(300), - }, - } - recordType := converters.GetRecordType(record.IP) - if recordType == privatedns.A { - set.RecordSetProperties.ARecords = &[]privatedns.ARecord{{ - Ipv4Address: &record.IP, - }} - } else if recordType == privatedns.AAAA { - set.RecordSetProperties.AaaaRecords = &[]privatedns.AaaaRecord{{ - Ipv6Address: &record.IP, - }} - } - err := s.client.CreateOrUpdateRecordSet(ctx, s.Scope.ResourceGroup(), zoneSpec.ZoneName, recordType, record.Hostname, set) - if err != nil { - return errors.Wrapf(err, "failed to create record %s in private DNS zone %s", record.Hostname, zoneSpec.ZoneName) - } - log.V(2).Info("successfully created record set", "private dns zone", zoneSpec.ZoneName, "record", record.Hostname) - } + ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureServiceReconcileTimeout) + defer cancel() + + zoneSpec, links, records := s.Scope.PrivateDNSSpec() + if zoneSpec == nil { + return nil + } + + managed, err := s.reconcileZone(ctx, zoneSpec) + if managed { + s.Scope.UpdatePutStatus(infrav1.PrivateDNSZoneReadyCondition, serviceName, err) + } + if err != nil { + return err + } + + managed, err = s.reconcileLinks(ctx, links) + if managed { + s.Scope.UpdatePutStatus(infrav1.PrivateDNSLinkReadyCondition, serviceName, err) + } + if err != nil { + return err } - return nil + + err = s.reconcileRecords(ctx, records) + s.Scope.UpdatePutStatus(infrav1.PrivateDNSRecordReadyCondition, serviceName, err) + return err } // 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") + ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.Service.Delete") defer done() - zoneSpec := s.Scope.PrivateDNSSpec() - if zoneSpec != nil { - for _, linkSpec := range zoneSpec.Links { - // 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) - 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) - if err != nil && azure.ResourceNotFound(err) { - // already deleted - return nil - } - if err != nil && !azure.ResourceNotFound(err) { - return errors.Wrapf(err, "failed to delete private dns zone %s in resource group %s", zoneSpec.ZoneName, s.Scope.ResourceGroup()) - } - log.V(2).Info("successfully deleted private dns zone", "private dns zone", zoneSpec.ZoneName) + ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureServiceReconcileTimeout) + defer cancel() + + zoneSpec, links, _ := s.Scope.PrivateDNSSpec() + if zoneSpec == nil { + return nil } - 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) + managed, err := s.deleteLinks(ctx, links) + if managed { + s.Scope.UpdateDeleteStatus(infrav1.PrivateDNSLinkReadyCondition, serviceName, err) + } if err != nil { - return false, err + return err } - tags := converters.MapToTags(zone.Tags) - return tags.HasOwned(s.Scope.ClusterName()), nil + + managed, err = s.deleteZone(ctx, zoneSpec) + if managed { + s.Scope.UpdateDeleteStatus(infrav1.PrivateDNSZoneReadyCondition, serviceName, err) + s.Scope.UpdateDeleteStatus(infrav1.PrivateDNSRecordReadyCondition, serviceName, err) + } + + return err } // 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) +func (s *Service) isVnetLinkManaged(ctx context.Context, spec azure.ResourceSpecGetter) (bool, error) { + result, err := s.vnetLinkGetter.Get(ctx, spec) if err != nil { return false, err } - tags := converters.MapToTags(zone.Tags) + + link, ok := result.(privatedns.VirtualNetworkLink) + if !ok { + return false, errors.Errorf("%T is not a privatedns.VirtualNetworkLink", link) + } + + tags := converters.MapToTags(link.Tags) return tags.HasOwned(s.Scope.ClusterName()), nil } -// IsManaged returns always returns true. -// TODO: separate private DNS and VNet links so we can implement the IsManaged method for each. +// IsManaged 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) IsManaged(ctx context.Context) (bool, error) { - return true, nil + zoneSpec, _, _ := s.Scope.PrivateDNSSpec() + if zoneSpec == nil { + return false, errors.Errorf("no private dns zone spec available") + } + + result, err := s.zoneGetter.Get(ctx, zoneSpec) + if err != nil { + return false, err + } + zone, ok := result.(privatedns.PrivateZone) + if !ok { + return false, errors.Errorf("%T is not a privatedns.PrivateZone", zone) + } + + 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 c5234e3235d..9c73d1d4af4 100644 --- a/azure/services/privatedns/privatedns_test.go +++ b/azure/services/privatedns/privatedns_test.go @@ -18,7 +18,6 @@ package privatedns import ( "context" - "net/http" "testing" "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns" @@ -26,413 +25,235 @@ import ( "github.com/Azure/go-autorest/autorest/to" "github.com/golang/mock/gomock" . "github.com/onsi/gomega" + "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/services/async/mock_async" "sigs.k8s.io/cluster-api-provider-azure/azure/services/privatedns/mock_privatedns" gomockinternal "sigs.k8s.io/cluster-api-provider-azure/internal/test/matchers/gomock" ) +const ( + zoneName = "my-zone" + resourceGroup = "my-rg" + vnetName = "my-vnet" + vnetResourceGroup = "my-vnet-rg" + linkName1 = "my-link-1" + linkName2 = "my-link-2" + clusterName = "my-cluster" + subscriptionID = "my-subscription-id" +) + +var ( + fakeZone = ZoneSpec{ + Name: zoneName, + ResourceGroup: resourceGroup, + ClusterName: clusterName, + AdditionalTags: nil, + } + + fakeLink1 = LinkSpec{ + Name: linkName1, + ZoneName: zoneName, + SubscriptionID: subscriptionID, + VNetResourceGroup: vnetResourceGroup, + VNetName: vnetName, + ResourceGroup: resourceGroup, + ClusterName: clusterName, + AdditionalTags: nil, + } + + fakeLink2 = LinkSpec{ + Name: linkName2, + ZoneName: zoneName, + SubscriptionID: subscriptionID, + VNetResourceGroup: vnetResourceGroup, + VNetName: vnetName, + ResourceGroup: resourceGroup, + ClusterName: clusterName, + AdditionalTags: nil, + } + + fakeRecord1 = RecordSpec{ + Record: infrav1.AddressRecord{Hostname: "my-host", IP: "10.0.0.8"}, + ZoneName: zoneName, + ResourceGroup: resourceGroup, + } + + fakeAzurePrivateZoneManaged = privatedns.PrivateZone{Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_" + clusterName: to.StringPtr("owned"), + }} + + fakeAzurePrivateZoneUnmanaged = privatedns.PrivateZone{} + + fakeAzureVnetLinkManaged = privatedns.VirtualNetworkLink{Tags: map[string]*string{ + "sigs.k8s.io_cluster-api-provider-azure_cluster_" + clusterName: to.StringPtr("owned"), + }} + + fakeAzureVnetLinkUnmanaged = privatedns.VirtualNetworkLink{} + + notDoneError = azure.NewOperationNotDoneError(&infrav1.Future{Type: "resourceType", ResourceGroup: resourceGroup, Name: "resourceName"}) + errFake = errors.New("this is an error") + notFoundError = autorest.DetailedError{StatusCode: 404} +) + func TestReconcilePrivateDNS(t *testing.T) { testcases := []struct { name string expectedError string - expect func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) + expect func(s *mock_privatedns.MockScopeMockRecorder, zoneReconiler, linksReconciler, recordsReconciler *mock_async.MockReconcilerMockRecorder, + zoneGetter, linkGetter *mock_async.MockGetterMockRecorder) }{ { name: "no private dns", expectedError: "", - expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { - s.PrivateDNSSpec().Return(nil) + expect: func(s *mock_privatedns.MockScopeMockRecorder, z, l, r *mock_async.MockReconcilerMockRecorder, zg, lg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(nil, nil, nil) }, }, { - name: "create ipv4 private dns successfully", + name: "create private dns with multiple links successfully", 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") - s.AdditionalTags().AnyTimes().Return(infrav1.Tags{}) - s.SubscriptionID().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{ - ID: to.StringPtr("/subscriptions/123/resourceGroups/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"), - }, - }) - 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"), - }, - }, - }, - }) + expect: func(s *mock_privatedns.MockScopeMockRecorder, z, l, r *mock_async.MockReconcilerMockRecorder, zg, lg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(fakeZone, []azure.ResourceSpecGetter{fakeLink1, fakeLink2}, []azure.ResourceSpecGetter{fakeRecord1}).Times(2) + zg.Get(gomockinternal.AContext(), fakeZone).Return(nil, notFoundError) + lg.Get(gomockinternal.AContext(), fakeLink1).Return(nil, notFoundError) + lg.Get(gomockinternal.AContext(), fakeLink2).Return(nil, notFoundError) + z.CreateResource(gomockinternal.AContext(), fakeZone, serviceName).Return(nil, nil) + l.CreateResource(gomockinternal.AContext(), fakeLink1, serviceName).Return(nil, nil) + l.CreateResource(gomockinternal.AContext(), fakeLink2, serviceName).Return(nil, nil) + r.CreateResource(gomockinternal.AContext(), fakeRecord1, serviceName).Return(nil, nil) + s.UpdatePutStatus(infrav1.PrivateDNSZoneReadyCondition, serviceName, nil) + s.UpdatePutStatus(infrav1.PrivateDNSLinkReadyCondition, serviceName, nil) + s.UpdatePutStatus(infrav1.PrivateDNSRecordReadyCondition, serviceName, nil) }, }, { - name: "create multiple ipv4 private dns successfully", - 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-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.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-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{ - ID: to.StringPtr("/subscriptions/123/resourceGroups/vnet-rg/providers/Microsoft.Network/virtualNetworks/my-vnet-1"), - }, - 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{ - ID: to.StringPtr("/subscriptions/123/resourceGroups/vnet-rg/providers/Microsoft.Network/virtualNetworks/my-vnet-2"), - }, - 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{ - TTL: to.Int64Ptr(300), - ARecords: &[]privatedns.ARecord{ - { - Ipv4Address: to.StringPtr("10.0.0.8"), - }, - }, - }, - }) + name: "zone creation in progress", + expectedError: "operation type resourceType on Azure resource my-rg/resourceName is not done", + expect: func(s *mock_privatedns.MockScopeMockRecorder, z, l, r *mock_async.MockReconcilerMockRecorder, zg, lg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(fakeZone, []azure.ResourceSpecGetter{fakeLink1, fakeLink2}, []azure.ResourceSpecGetter{fakeRecord1}).Times(2) + zg.Get(gomockinternal.AContext(), fakeZone).Return(nil, notFoundError) + z.CreateResource(gomockinternal.AContext(), fakeZone, serviceName).Return(nil, notDoneError) + s.UpdatePutStatus(infrav1.PrivateDNSZoneReadyCondition, serviceName, notDoneError) }, }, { - name: "create ipv6 private dns successfully", - 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-2", - IP: "2603:1030:805:2::b", - }, - }, - }) - s.ResourceGroup().AnyTimes().Return("my-rg") - 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{ - ID: to.StringPtr("/subscriptions/123/resourceGroups/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"), - }, - }) - 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: "zone creation fails", + expectedError: "this is an error", + expect: func(s *mock_privatedns.MockScopeMockRecorder, z, l, r *mock_async.MockReconcilerMockRecorder, zg, lg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(fakeZone, []azure.ResourceSpecGetter{fakeLink1, fakeLink2}, []azure.ResourceSpecGetter{fakeRecord1}).Times(2) + zg.Get(gomockinternal.AContext(), fakeZone).Return(nil, notFoundError) + z.CreateResource(gomockinternal.AContext(), fakeZone, serviceName).Return(nil, errFake) + s.UpdatePutStatus(infrav1.PrivateDNSZoneReadyCondition, serviceName, errFake) }, }, { - name: "create multiple ipv6 private dns successfully", - 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-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.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-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{ - ID: to.StringPtr("/subscriptions/123/resourceGroups/vnet-rg/providers/Microsoft.Network/virtualNetworks/my-vnet-1"), - }, - 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{ - ID: to.StringPtr("/subscriptions/123/resourceGroups/vnet-rg/providers/Microsoft.Network/virtualNetworks/my-vnet-2"), - }, - 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{ - TTL: to.Int64Ptr(300), - AaaaRecords: &[]privatedns.AaaaRecord{ - { - Ipv6Address: to.StringPtr("2603:1030:805:2::b"), - }, - }, - }, - }) + name: "unmanaged zone does not update ready condition", + expect: func(s *mock_privatedns.MockScopeMockRecorder, z, l, r *mock_async.MockReconcilerMockRecorder, zg, lg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(fakeZone, []azure.ResourceSpecGetter{fakeLink1, fakeLink2}, []azure.ResourceSpecGetter{fakeRecord1}).Times(2) + zg.Get(gomockinternal.AContext(), fakeZone).Return(fakeAzurePrivateZoneUnmanaged, nil) + s.ClusterName() + lg.Get(gomockinternal.AContext(), fakeLink1).Return(false, notFoundError) + lg.Get(gomockinternal.AContext(), fakeLink2).Return(false, notFoundError) + l.CreateResource(gomockinternal.AContext(), fakeLink1, serviceName).Return(nil, nil) + l.CreateResource(gomockinternal.AContext(), fakeLink2, serviceName).Return(nil, nil) + r.CreateResource(gomockinternal.AContext(), fakeRecord1, serviceName).Return(nil, nil) + s.UpdatePutStatus(infrav1.PrivateDNSLinkReadyCondition, serviceName, nil) + s.UpdatePutStatus(infrav1.PrivateDNSRecordReadyCondition, serviceName, nil) + }, + }, + { + name: "link 1 creation fails but still proceeds to link 2, and returns the error", + expectedError: "this is an error", + expect: func(s *mock_privatedns.MockScopeMockRecorder, z, l, r *mock_async.MockReconcilerMockRecorder, zg, lg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(fakeZone, []azure.ResourceSpecGetter{fakeLink1, fakeLink2}, []azure.ResourceSpecGetter{fakeRecord1}).Times(2) + zg.Get(gomockinternal.AContext(), fakeZone).Return(nil, notFoundError) + lg.Get(gomockinternal.AContext(), fakeLink1).Return(nil, notFoundError) + lg.Get(gomockinternal.AContext(), fakeLink2).Return(nil, notFoundError) + z.CreateResource(gomockinternal.AContext(), fakeZone, serviceName).Return(nil, nil) + l.CreateResource(gomockinternal.AContext(), fakeLink1, serviceName).Return(nil, errFake) + l.CreateResource(gomockinternal.AContext(), fakeLink2, serviceName).Return(nil, nil) + s.UpdatePutStatus(infrav1.PrivateDNSZoneReadyCondition, serviceName, nil) + s.UpdatePutStatus(infrav1.PrivateDNSLinkReadyCondition, serviceName, errFake) }, }, { - 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.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") - 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{ - ID: to.StringPtr("/subscriptions/123/resourceGroups/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"), - }, - }).Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) + name: "link 2 creation fails", + expectedError: "this is an error", + expect: func(s *mock_privatedns.MockScopeMockRecorder, z, l, r *mock_async.MockReconcilerMockRecorder, zg, lg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(fakeZone, []azure.ResourceSpecGetter{fakeLink1, fakeLink2}, []azure.ResourceSpecGetter{fakeRecord1}).Times(2) + zg.Get(gomockinternal.AContext(), fakeZone).Return(nil, notFoundError) + lg.Get(gomockinternal.AContext(), fakeLink1).Return(nil, notFoundError) + lg.Get(gomockinternal.AContext(), fakeLink2).Return(nil, notFoundError) + z.CreateResource(gomockinternal.AContext(), fakeZone, serviceName).Return(nil, nil) + l.CreateResource(gomockinternal.AContext(), fakeLink1, serviceName).Return(nil, nil) + l.CreateResource(gomockinternal.AContext(), fakeLink2, serviceName).Return(nil, errFake) + s.UpdatePutStatus(infrav1.PrivateDNSZoneReadyCondition, serviceName, nil) + s.UpdatePutStatus(infrav1.PrivateDNSLinkReadyCondition, serviceName, errFake) }, }, { - 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.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.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-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{ - ID: to.StringPtr("/subscriptions/123/resourceGroups/vnet-rg/providers/Microsoft.Network/virtualNetworks/my-vnet-1"), - }, - 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{ - ID: to.StringPtr("/subscriptions/123/resourceGroups/vnet-rg/providers/Microsoft.Network/virtualNetworks/my-vnet-2"), - }, - 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")) + name: "link 1 is long running, link 2 fails, it returns the failure of link2", + expectedError: "this is an error", + expect: func(s *mock_privatedns.MockScopeMockRecorder, z, l, r *mock_async.MockReconcilerMockRecorder, zg, lg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(fakeZone, []azure.ResourceSpecGetter{fakeLink1, fakeLink2}, []azure.ResourceSpecGetter{fakeRecord1}).Times(2) + zg.Get(gomockinternal.AContext(), fakeZone).Return(nil, notFoundError) + lg.Get(gomockinternal.AContext(), fakeLink1).Return(nil, notFoundError) + lg.Get(gomockinternal.AContext(), fakeLink2).Return(nil, notFoundError) + z.CreateResource(gomockinternal.AContext(), fakeZone, serviceName).Return(nil, nil) + l.CreateResource(gomockinternal.AContext(), fakeLink1, serviceName).Return(nil, notDoneError) + l.CreateResource(gomockinternal.AContext(), fakeLink2, serviceName).Return(nil, errFake) + s.UpdatePutStatus(infrav1.PrivateDNSZoneReadyCondition, serviceName, nil) + s.UpdatePutStatus(infrav1.PrivateDNSLinkReadyCondition, serviceName, errFake) + }, + }, + { + name: "unmanaged link does not update ready condition", + expect: func(s *mock_privatedns.MockScopeMockRecorder, z, l, r *mock_async.MockReconcilerMockRecorder, zg, lg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(fakeZone, []azure.ResourceSpecGetter{fakeLink1, fakeLink2}, []azure.ResourceSpecGetter{fakeRecord1}).Times(2) + zg.Get(gomockinternal.AContext(), fakeZone).Return(nil, notFoundError) + lg.Get(gomockinternal.AContext(), fakeLink1).Return(fakeAzureVnetLinkUnmanaged, nil) + s.ClusterName() + lg.Get(gomockinternal.AContext(), fakeLink2).Return(fakeAzureVnetLinkUnmanaged, nil) + s.ClusterName() + z.CreateResource(gomockinternal.AContext(), fakeZone, serviceName).Return(nil, nil) + r.CreateResource(gomockinternal.AContext(), fakeRecord1, serviceName).Return(nil, nil) + s.UpdatePutStatus(infrav1.PrivateDNSZoneReadyCondition, serviceName, nil) + s.UpdatePutStatus(infrav1.PrivateDNSRecordReadyCondition, serviceName, nil) + }, + }, + { + name: "vnet link is considered managed if at least one of the links is managed", + expect: func(s *mock_privatedns.MockScopeMockRecorder, z, l, r *mock_async.MockReconcilerMockRecorder, zg, lg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(fakeZone, []azure.ResourceSpecGetter{fakeLink1, fakeLink2}, []azure.ResourceSpecGetter{fakeRecord1}).Times(2) + zg.Get(gomockinternal.AContext(), fakeZone).Return(nil, notFoundError) + lg.Get(gomockinternal.AContext(), fakeLink1).Return(fakeAzureVnetLinkUnmanaged, nil) + s.ClusterName() + lg.Get(gomockinternal.AContext(), fakeLink2).Return(nil, notFoundError) + z.CreateResource(gomockinternal.AContext(), fakeZone, serviceName).Return(nil, nil) + l.CreateResource(gomockinternal.AContext(), fakeLink2, serviceName).Return(nil, nil) + r.CreateResource(gomockinternal.AContext(), fakeRecord1, serviceName).Return(nil, nil) + s.UpdatePutStatus(infrav1.PrivateDNSZoneReadyCondition, serviceName, nil) + s.UpdatePutStatus(infrav1.PrivateDNSLinkReadyCondition, serviceName, nil) + s.UpdatePutStatus(infrav1.PrivateDNSRecordReadyCondition, serviceName, nil) + }, + }, + { + name: "record creation fails", + expectedError: "this is an error", + expect: func(s *mock_privatedns.MockScopeMockRecorder, z, l, r *mock_async.MockReconcilerMockRecorder, zg, lg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(fakeZone, []azure.ResourceSpecGetter{fakeLink1, fakeLink2}, []azure.ResourceSpecGetter{fakeRecord1}).Times(2) + zg.Get(gomockinternal.AContext(), fakeZone).Return(nil, notFoundError) + lg.Get(gomockinternal.AContext(), fakeLink1).Return(nil, notFoundError) + lg.Get(gomockinternal.AContext(), fakeLink2).Return(nil, notFoundError) + z.CreateResource(gomockinternal.AContext(), fakeZone, serviceName).Return(nil, nil) + l.CreateResource(gomockinternal.AContext(), fakeLink1, serviceName).Return(nil, nil) + l.CreateResource(gomockinternal.AContext(), fakeLink2, serviceName).Return(nil, nil) + r.CreateResource(gomockinternal.AContext(), fakeRecord1, serviceName).Return(nil, errFake) + s.UpdatePutStatus(infrav1.PrivateDNSZoneReadyCondition, serviceName, nil) + s.UpdatePutStatus(infrav1.PrivateDNSLinkReadyCondition, serviceName, nil) + s.UpdatePutStatus(infrav1.PrivateDNSRecordReadyCondition, serviceName, errFake) }, }, } @@ -446,13 +267,22 @@ func TestReconcilePrivateDNS(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() scopeMock := mock_privatedns.NewMockScope(mockCtrl) - clientMock := mock_privatedns.NewMockclient(mockCtrl) + zoneReconcilerMock := mock_async.NewMockReconciler(mockCtrl) + vnetLinkReconcilerMock := mock_async.NewMockReconciler(mockCtrl) + recordReconcilerMock := mock_async.NewMockReconciler(mockCtrl) + zoneGetterMock := mock_async.NewMockGetter(mockCtrl) + vnetLinkGetterMock := mock_async.NewMockGetter(mockCtrl) - tc.expect(scopeMock.EXPECT(), clientMock.EXPECT()) + tc.expect(scopeMock.EXPECT(), zoneReconcilerMock.EXPECT(), vnetLinkReconcilerMock.EXPECT(), recordReconcilerMock.EXPECT(), + zoneGetterMock.EXPECT(), vnetLinkGetterMock.EXPECT()) s := &Service{ - Scope: scopeMock, - client: clientMock, + Scope: scopeMock, + zoneReconciler: zoneReconcilerMock, + vnetLinkReconciler: vnetLinkReconcilerMock, + recordReconciler: recordReconcilerMock, + zoneGetter: zoneGetterMock, + vnetLinkGetter: vnetLinkGetterMock, } err := s.Reconcile(context.TODO()) @@ -470,459 +300,144 @@ func TestDeletePrivateDNS(t *testing.T) { testcases := []struct { name string expectedError string - expect func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) + expect func(s *mock_privatedns.MockScopeMockRecorder, linkReconciler, zoneReconciler *mock_async.MockReconcilerMockRecorder, + linkGetter, zoneGetter *mock_async.MockGetterMockRecorder) }{ { name: "no private dns", expectedError: "", - expect: func(s *mock_privatedns.MockScopeMockRecorder, m *mock_privatedns.MockclientMockRecorder) { - s.PrivateDNSSpec().Return(nil) - }, - }, - { - 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{ - 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").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: "", - 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-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.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") + expect: func(s *mock_privatedns.MockScopeMockRecorder, lr, zr *mock_async.MockReconcilerMockRecorder, lg, zg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(nil, nil, nil) }, }, { - name: "link already deleted", + name: "dns and links deletion succeeds", 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"). - 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") + expect: func(s *mock_privatedns.MockScopeMockRecorder, lr, zr *mock_async.MockReconcilerMockRecorder, lg, zg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(fakeZone, []azure.ResourceSpecGetter{fakeLink1, fakeLink2}, []azure.ResourceSpecGetter{fakeRecord1}).Times(2) + + lg.Get(gomockinternal.AContext(), fakeLink1).Return(fakeAzureVnetLinkManaged, nil) + s.ClusterName().Return(clusterName) + lr.DeleteResource(gomockinternal.AContext(), fakeLink1, serviceName).Return(nil) + lg.Get(gomockinternal.AContext(), fakeLink2).Return(fakeAzureVnetLinkManaged, nil) + s.ClusterName().Return(clusterName) + lr.DeleteResource(gomockinternal.AContext(), fakeLink2, serviceName).Return(nil) + + zg.Get(gomockinternal.AContext(), fakeZone).Return(fakeAzurePrivateZoneManaged, nil) + s.ClusterName().Return(clusterName) + zr.DeleteResource(gomockinternal.AContext(), fakeZone, serviceName).Return(nil) + s.UpdateDeleteStatus(infrav1.PrivateDNSLinkReadyCondition, serviceName, nil) + s.UpdateDeleteStatus(infrav1.PrivateDNSZoneReadyCondition, serviceName, nil) + s.UpdateDeleteStatus(infrav1.PrivateDNSRecordReadyCondition, serviceName, nil) }, }, { - name: "one link already deleted with multiple links", + name: "skips if zone and links are unmanaged", 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-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.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{}, 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") + expect: func(s *mock_privatedns.MockScopeMockRecorder, lr, zr *mock_async.MockReconcilerMockRecorder, lg, zg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(fakeZone, []azure.ResourceSpecGetter{fakeLink1, fakeLink2}, []azure.ResourceSpecGetter{fakeRecord1}).Times(2) + + lg.Get(gomockinternal.AContext(), fakeLink1).Return(fakeAzureVnetLinkUnmanaged, nil) + s.ClusterName().Return(clusterName) + lg.Get(gomockinternal.AContext(), fakeLink2).Return(fakeAzureVnetLinkUnmanaged, nil) + s.ClusterName().Return(clusterName) + + zg.Get(gomockinternal.AContext(), fakeZone).Return(fakeAzurePrivateZoneUnmanaged, nil) + s.ClusterName().Return(clusterName) }, }, { - name: "zone and all vnet links already deleted", + name: "skips if unmanaged, but deletes the next resource if it is managed", 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"). - 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")) + expect: func(s *mock_privatedns.MockScopeMockRecorder, lr, zr *mock_async.MockReconcilerMockRecorder, lg, zg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(fakeZone, []azure.ResourceSpecGetter{fakeLink1, fakeLink2}, []azure.ResourceSpecGetter{fakeRecord1}).Times(2) + + lg.Get(gomockinternal.AContext(), fakeLink1).Return(fakeAzureVnetLinkUnmanaged, nil) + s.ClusterName().Return(clusterName) + lg.Get(gomockinternal.AContext(), fakeLink2).Return(fakeAzureVnetLinkManaged, nil) + s.ClusterName().Return(clusterName) + lr.DeleteResource(gomockinternal.AContext(), fakeLink2, serviceName).Return(nil) + + zg.Get(gomockinternal.AContext(), fakeZone).Return(fakeAzurePrivateZoneManaged, nil) + s.ClusterName().Return(clusterName) + zr.DeleteResource(gomockinternal.AContext(), fakeZone, serviceName).Return(nil) + s.UpdateDeleteStatus(infrav1.PrivateDNSLinkReadyCondition, serviceName, nil) + s.UpdateDeleteStatus(infrav1.PrivateDNSZoneReadyCondition, serviceName, nil) + s.UpdateDeleteStatus(infrav1.PrivateDNSRecordReadyCondition, serviceName, nil) }, }, { - name: "error while trying to delete the link", - expectedError: "failed to delete virtual network link my-vnet 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.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").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")) + name: "link1 is deleted, link2 is long running. It returns not done error", + expectedError: "operation type resourceType on Azure resource my-rg/resourceName is not done", + expect: func(s *mock_privatedns.MockScopeMockRecorder, lr, zr *mock_async.MockReconcilerMockRecorder, lg, zg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(fakeZone, []azure.ResourceSpecGetter{fakeLink1, fakeLink2}, []azure.ResourceSpecGetter{fakeRecord1}) + + lg.Get(gomockinternal.AContext(), fakeLink1).Return(fakeAzureVnetLinkManaged, nil) + s.ClusterName().Return(clusterName) + lr.DeleteResource(gomockinternal.AContext(), fakeLink1, serviceName).Return(notDoneError) + lg.Get(gomockinternal.AContext(), fakeLink2).Return(fakeAzureVnetLinkManaged, nil) + s.ClusterName().Return(clusterName) + lr.DeleteResource(gomockinternal.AContext(), fakeLink2, serviceName).Return(nil) + s.UpdateDeleteStatus(infrav1.PrivateDNSLinkReadyCondition, serviceName, notDoneError) }, }, { - 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.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.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")) + name: "link1 deletion fails and link2 is long running, returns the more pressing error", + expectedError: "this is an error", + expect: func(s *mock_privatedns.MockScopeMockRecorder, lr, zr *mock_async.MockReconcilerMockRecorder, lg, zg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(fakeZone, []azure.ResourceSpecGetter{fakeLink1, fakeLink2}, []azure.ResourceSpecGetter{fakeRecord1}) + + lg.Get(gomockinternal.AContext(), fakeLink1).Return(fakeAzureVnetLinkManaged, nil) + s.ClusterName().Return(clusterName) + lr.DeleteResource(gomockinternal.AContext(), fakeLink1, serviceName).Return(errFake) + lg.Get(gomockinternal.AContext(), fakeLink2).Return(fakeAzureVnetLinkManaged, nil) + s.ClusterName().Return(clusterName) + lr.DeleteResource(gomockinternal.AContext(), fakeLink2, serviceName).Return(notDoneError) + s.UpdateDeleteStatus(infrav1.PrivateDNSLinkReadyCondition, serviceName, errFake) }, }, { - 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.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").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")) + name: "links are deleted, zone is long running", + expectedError: "operation type resourceType on Azure resource my-rg/resourceName is not done", + expect: func(s *mock_privatedns.MockScopeMockRecorder, lr, zr *mock_async.MockReconcilerMockRecorder, lg, zg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(fakeZone, []azure.ResourceSpecGetter{fakeLink1, fakeLink2}, []azure.ResourceSpecGetter{fakeRecord1}).Times(2) + + lg.Get(gomockinternal.AContext(), fakeLink1).Return(fakeAzureVnetLinkManaged, nil) + s.ClusterName().Return(clusterName) + lr.DeleteResource(gomockinternal.AContext(), fakeLink1, serviceName).Return(nil) + lg.Get(gomockinternal.AContext(), fakeLink2).Return(fakeAzureVnetLinkManaged, nil) + s.ClusterName().Return(clusterName) + lr.DeleteResource(gomockinternal.AContext(), fakeLink2, serviceName).Return(nil) + + zg.Get(gomockinternal.AContext(), fakeZone).Return(fakeAzurePrivateZoneManaged, nil) + s.ClusterName().Return(clusterName) + zr.DeleteResource(gomockinternal.AContext(), fakeZone, serviceName).Return(notDoneError) + + s.UpdateDeleteStatus(infrav1.PrivateDNSLinkReadyCondition, serviceName, nil) + s.UpdateDeleteStatus(infrav1.PrivateDNSZoneReadyCondition, serviceName, notDoneError) + s.UpdateDeleteStatus(infrav1.PrivateDNSRecordReadyCondition, serviceName, notDoneError) }, }, { - 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.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.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")) + name: "links are deleted, zone deletion fails with error", + expectedError: "this is an error", + expect: func(s *mock_privatedns.MockScopeMockRecorder, lr, zr *mock_async.MockReconcilerMockRecorder, lg, zg *mock_async.MockGetterMockRecorder) { + s.PrivateDNSSpec().Return(fakeZone, []azure.ResourceSpecGetter{fakeLink1, fakeLink2}, []azure.ResourceSpecGetter{fakeRecord1}).Times(2) + + lg.Get(gomockinternal.AContext(), fakeLink1).Return(fakeAzureVnetLinkManaged, nil) + s.ClusterName().Return(clusterName) + lr.DeleteResource(gomockinternal.AContext(), fakeLink1, serviceName).Return(nil) + lg.Get(gomockinternal.AContext(), fakeLink2).Return(fakeAzureVnetLinkManaged, nil) + s.ClusterName().Return(clusterName) + lr.DeleteResource(gomockinternal.AContext(), fakeLink2, serviceName).Return(nil) + + zg.Get(gomockinternal.AContext(), fakeZone).Return(fakeAzurePrivateZoneManaged, nil) + s.ClusterName().Return(clusterName) + zr.DeleteResource(gomockinternal.AContext(), fakeZone, serviceName).Return(errFake) + + s.UpdateDeleteStatus(infrav1.PrivateDNSLinkReadyCondition, serviceName, nil) + s.UpdateDeleteStatus(infrav1.PrivateDNSZoneReadyCondition, serviceName, errFake) + s.UpdateDeleteStatus(infrav1.PrivateDNSRecordReadyCondition, serviceName, errFake) }, }, } @@ -936,13 +451,20 @@ func TestDeletePrivateDNS(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() scopeMock := mock_privatedns.NewMockScope(mockCtrl) - clientMock := mock_privatedns.NewMockclient(mockCtrl) + vnetLinkReconcilerMock := mock_async.NewMockReconciler(mockCtrl) + zoneReconcilerMock := mock_async.NewMockReconciler(mockCtrl) + vnetLinkGetterMock := mock_async.NewMockGetter(mockCtrl) + zoneGetterMock := mock_async.NewMockGetter(mockCtrl) - tc.expect(scopeMock.EXPECT(), clientMock.EXPECT()) + tc.expect(scopeMock.EXPECT(), vnetLinkReconcilerMock.EXPECT(), zoneReconcilerMock.EXPECT(), + vnetLinkGetterMock.EXPECT(), zoneGetterMock.EXPECT()) s := &Service{ - Scope: scopeMock, - client: clientMock, + Scope: scopeMock, + zoneReconciler: zoneReconcilerMock, + vnetLinkReconciler: vnetLinkReconcilerMock, + zoneGetter: zoneGetterMock, + vnetLinkGetter: vnetLinkGetterMock, } err := s.Delete(context.TODO()) diff --git a/azure/services/privatedns/record_client.go b/azure/services/privatedns/record_client.go new file mode 100644 index 00000000000..387b6ccb83d --- /dev/null +++ b/azure/services/privatedns/record_client.go @@ -0,0 +1,92 @@ +/* +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" + azureautorest "github.com/Azure/go-autorest/autorest/azure" + "github.com/pkg/errors" + "sigs.k8s.io/cluster-api-provider-azure/azure" + "sigs.k8s.io/cluster-api-provider-azure/util/tele" +) + +// azureRecordsClient contains the Azure go-sdk Client for record sets. +type azureRecordsClient struct { + recordsets privatedns.RecordSetsClient +} + +// newRecordSetsClient creates a new record sets client from subscription ID. +func newRecordSetsClient(auth azure.Authorizer) *azureRecordsClient { + recordsClient := privatedns.NewRecordSetsClientWithBaseURI(auth.BaseURI(), auth.SubscriptionID()) + azure.SetAutoRestClientDefaults(&recordsClient.Client, auth.Authorizer()) + return &azureRecordsClient{ + recordsets: recordsClient, + } +} + +// CreateOrUpdateAsync creates or updates a record asynchronously. +// Creating a record set is not a long running operation, so we don't ever return a future. +func (arc *azureRecordsClient) CreateOrUpdateAsync(ctx context.Context, spec azure.ResourceSpecGetter, parameters interface{}) (result interface{}, future azureautorest.FutureAPI, err error) { + ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.azureRecordsClient.CreateOrUpdateAsync") + defer done() + + set, ok := parameters.(privatedns.RecordSet) + if !ok { + return nil, nil, errors.Errorf("%T is not a privatedns.RecordSet", parameters) + } + + // Determine record type. + var ( + recordType privatedns.RecordType + aRecords = set.RecordSetProperties.ARecords + aaaRecords = set.RecordSetProperties.AaaaRecords + ) + if aRecords != nil && len(*aRecords) > 0 && (*aRecords)[0].Ipv4Address != nil { + recordType = privatedns.A + } else if aaaRecords != nil && len(*aaaRecords) > 0 && (*aaaRecords)[0].Ipv6Address != nil { + recordType = privatedns.AAAA + } + + recordSet, err := arc.recordsets.CreateOrUpdate(ctx, spec.ResourceGroupName(), spec.OwnerResourceName(), recordType, spec.ResourceName(), set, "", "") + if err != nil { + return nil, nil, err + } + + return recordSet, nil, err +} + +// Get gets the specified record set. Noop for records. +func (arc *azureRecordsClient) Get(ctx context.Context, spec azure.ResourceSpecGetter) (result interface{}, err error) { + return nil, nil +} + +// DeleteAsync deletes a record asynchronously. Noop for records. +func (arc *azureRecordsClient) DeleteAsync(ctx context.Context, spec azure.ResourceSpecGetter) (future azureautorest.FutureAPI, err error) { + return nil, nil +} + +// IsDone returns true if the long-running operation has completed. Noop for records. +func (arc *azureRecordsClient) IsDone(ctx context.Context, future azureautorest.FutureAPI) (isDone bool, err error) { + return true, nil +} + +// Result fetches the result of a long-running operation future. Noop for records. +func (arc *azureRecordsClient) Result(ctx context.Context, future azureautorest.FutureAPI, futureType string) (result interface{}, err error) { + return nil, nil +} diff --git a/azure/services/privatedns/record_reconciler.go b/azure/services/privatedns/record_reconciler.go new file mode 100644 index 00000000000..9330c19aeb0 --- /dev/null +++ b/azure/services/privatedns/record_reconciler.go @@ -0,0 +1,44 @@ +/* +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" + + "sigs.k8s.io/cluster-api-provider-azure/azure" + "sigs.k8s.io/cluster-api-provider-azure/util/tele" +) + +func (s *Service) reconcileRecords(ctx context.Context, records []azure.ResourceSpecGetter) error { + ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.Service.reconcileRecords") + defer done() + + var resErr error + + // We go through the list of links to reconcile each one, independently of the result of the previous one. + // If multiple errors occur, we return the most pressing one. + // Order of precedence (highest -> lowest) is: error that is not an operationNotDoneError (i.e. error creating) -> operationNotDoneError (i.e. creating in progress) -> no error (i.e. created) + for _, recordSpec := range records { + if _, err := s.recordReconciler.CreateResource(ctx, recordSpec, serviceName); err != nil { + if !azure.IsOperationNotDoneError(err) || resErr == nil { + resErr = err + } + } + } + + return resErr +} diff --git a/azure/services/privatedns/record_spec.go b/azure/services/privatedns/record_spec.go new file mode 100644 index 00000000000..e37e6321c4f --- /dev/null +++ b/azure/services/privatedns/record_spec.go @@ -0,0 +1,75 @@ +/* +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 ( + "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/converters" +) + +type RecordSpec struct { + Record infrav1.AddressRecord + ZoneName string + ResourceGroup string +} + +// ResourceName returns the name of a record set. +func (s RecordSpec) ResourceName() string { + return s.Record.Hostname +} + +// OwnerResourceName returns the zone name of a record set. +func (s RecordSpec) OwnerResourceName() string { + return s.ZoneName +} + +// ResourceGroupName returns the name of the resource group of a record set. +func (s RecordSpec) ResourceGroupName() string { + return s.ResourceGroup +} + +// Parameters returns the parameters for a record set. +func (s RecordSpec) Parameters(existing interface{}) (params interface{}, err error) { + if existing != nil { + if _, ok := existing.(privatedns.RecordSet); !ok { + return nil, errors.Errorf("%T is not a privatedns.RecordSet", existing) + } + } + set := privatedns.RecordSet{ + RecordSetProperties: &privatedns.RecordSetProperties{ + TTL: to.Int64Ptr(300), + }, + } + recordType := converters.GetRecordType(s.Record.IP) + switch recordType { + case privatedns.A: + set.RecordSetProperties.ARecords = &[]privatedns.ARecord{{ + Ipv4Address: &s.Record.IP, + }} + case privatedns.AAAA: + set.RecordSetProperties.AaaaRecords = &[]privatedns.AaaaRecord{{ + Ipv6Address: &s.Record.IP, + }} + default: + return nil, errors.Errorf("unknown record type %s", recordType) + } + + return set, nil +} diff --git a/azure/services/privatedns/record_spec_test.go b/azure/services/privatedns/record_spec_test.go new file mode 100644 index 00000000000..a591fa046ef --- /dev/null +++ b/azure/services/privatedns/record_spec_test.go @@ -0,0 +1,117 @@ +/* +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" +) + +var ( + 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 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/services/privatedns/zone_client.go b/azure/services/privatedns/zone_client.go new file mode 100644 index 00000000000..37d8a6b0bf8 --- /dev/null +++ b/azure/services/privatedns/zone_client.go @@ -0,0 +1,158 @@ +/* +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" + "encoding/json" + + "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns" + azureautorest "github.com/Azure/go-autorest/autorest/azure" + "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/reconciler" + "sigs.k8s.io/cluster-api-provider-azure/util/tele" +) + +// azureZonesClient contains the Azure go-sdk Client for private dns zone. +type azureZonesClient struct { + privatezones privatedns.PrivateZonesClient +} + +// newPrivateZonesClient creates a new private zones client from subscription ID. +func newPrivateZonesClient(auth azure.Authorizer) *azureZonesClient { + c := privatedns.NewPrivateZonesClientWithBaseURI(auth.BaseURI(), auth.SubscriptionID()) + azure.SetAutoRestClientDefaults(&c.Client, auth.Authorizer()) + return &azureZonesClient{ + privatezones: c, + } +} + +// CreateOrUpdateAsync creates or updates a private dns zone asynchronously. +// It sends a PUT request to Azure and if accepted without error, the func will return a Future which can be used to track the ongoing +// progress of the operation. +func (azc *azureZonesClient) CreateOrUpdateAsync(ctx context.Context, spec azure.ResourceSpecGetter, parameters interface{}) (result interface{}, future azureautorest.FutureAPI, err error) { + ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.azureZonesClient.CreateOrUpdateAsync") + defer done() + + zone, ok := parameters.(privatedns.PrivateZone) + if !ok { + return nil, nil, errors.Errorf("%T is not a privatedns.PrivateZone", parameters) + } + + createFuture, err := azc.privatezones.CreateOrUpdate(ctx, spec.ResourceGroupName(), spec.ResourceName(), zone, "", "") + if err != nil { + return nil, nil, err + } + + ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureCallTimeout) + defer cancel() + + err = createFuture.WaitForCompletionRef(ctx, azc.privatezones.Client) + if err != nil { + // if an error occurs, return the future. + // this means the long-running operation didn't finish in the specified timeout. + return nil, &createFuture, err + } + result, err = createFuture.Result(azc.privatezones) + // if the operation completed, return a nil future + return result, nil, err +} + +// Get gets the specified private dns zone. +func (azc *azureZonesClient) Get(ctx context.Context, spec azure.ResourceSpecGetter) (result interface{}, err error) { + ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.azureZonesClient.Get") + defer done() + zone, err := azc.privatezones.Get(ctx, spec.ResourceGroupName(), spec.ResourceName()) + if err != nil { + return privatedns.PrivateZone{}, err + } + return zone, nil +} + +// DeleteAsync deletes a private dns zone asynchronously. DeleteAsync sends a DELETE +// request to Azure and if accepted without error, the func will return a Future which can be used to track the ongoing +// progress of the operation. +func (azc *azureZonesClient) DeleteAsync(ctx context.Context, spec azure.ResourceSpecGetter) (future azureautorest.FutureAPI, err error) { + ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.azureZonesClient.DeleteAsync") + defer done() + + deleteFuture, err := azc.privatezones.Delete(ctx, spec.ResourceGroupName(), spec.ResourceName(), "") + if err != nil { + return nil, err + } + + ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureCallTimeout) + defer cancel() + + err = deleteFuture.WaitForCompletionRef(ctx, azc.privatezones.Client) + if err != nil { + // if an error occurs, return the future. + // this means the long-running operation didn't finish in the specified timeout. + return &deleteFuture, err + } + _, err = deleteFuture.Result(azc.privatezones) + // if the operation completed, return a nil future. + return nil, err +} + +// IsDone returns true if the long-running operation has completed. +func (azc *azureZonesClient) IsDone(ctx context.Context, future azureautorest.FutureAPI) (isDone bool, err error) { + ctx, _, done := tele.StartSpanWithLogger(ctx, "privatedns.azureZonesClient.IsDone") + defer done() + + isDone, err = future.DoneWithContext(ctx, azc.privatezones) + if err != nil { + return false, errors.Wrap(err, "failed checking if the operation was complete") + } + + return isDone, nil +} + +// Result fetches the result of a long-running operation future. +func (azc *azureZonesClient) Result(ctx context.Context, future azureautorest.FutureAPI, futureType string) (result interface{}, err error) { + _, _, done := tele.StartSpanWithLogger(ctx, "privatedns.azureZonesClient.Result") + defer done() + + if future == nil { + return nil, errors.Errorf("cannot get result from nil future") + } + + switch futureType { + case infrav1.PutFuture: + // Marshal and Unmarshal the future to put it into the correct future type so we can access the Result function. + // Unfortunately the FutureAPI can't be casted directly to PrivateZonesCreateOrUpdateFuture because it is a azureautorest.Future, which doesn't implement the Result function. See PR #1686 for discussion on alternatives. + // It was converted back to a generic azureautorest.Future from the CAPZ infrav1.Future type stored in Status: https://github.com/kubernetes-sigs/cluster-api-provider-azure/blob/main/azure/converters/futures.go#L49. + var createFuture *privatedns.PrivateZonesCreateOrUpdateFuture + jsonData, err := future.MarshalJSON() + if err != nil { + return nil, errors.Wrap(err, "failed to marshal future") + } + if err := json.Unmarshal(jsonData, &createFuture); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal future data") + } + return createFuture.Result(azc.privatezones) + + case infrav1.DeleteFuture: + // Delete does not return a result private dns zone. + return nil, nil + + default: + return nil, errors.Errorf("unknown future type %q", futureType) + } +} diff --git a/azure/services/privatedns/zone_reconciler.go b/azure/services/privatedns/zone_reconciler.go new file mode 100644 index 00000000000..96f857dfd73 --- /dev/null +++ b/azure/services/privatedns/zone_reconciler.go @@ -0,0 +1,78 @@ +/* +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/pkg/errors" + "sigs.k8s.io/cluster-api-provider-azure/azure" + "sigs.k8s.io/cluster-api-provider-azure/util/tele" +) + +func (s *Service) reconcileZone(ctx context.Context, zoneSpec azure.ResourceSpecGetter) (managed bool, err error) { + ctx, log, done := tele.StartSpanWithLogger(ctx, "privatedns.Service.reconcileZone") + defer done() + + managed, err = s.IsManaged(ctx) + if err != nil { + if azure.ResourceNotFound(err) { + managed = true + } else { + return managed, err + } + } + + if !managed { + log.V(1).Info("Skipping reconciliation of unmanaged private DNS zone", "private DNS", zoneSpec.ResourceName()) + // TODO: Remove this log in future release. This is only required because older clusters created before https://github.com/kubernetes-sigs/cluster-api-provider-azure/pull/1791 will not have capz ownership tags. + 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.ResourceName()) + return managed, nil + } + + _, err = s.zoneReconciler.CreateResource(ctx, zoneSpec, serviceName) + return managed, err +} + +func (s *Service) deleteZone(ctx context.Context, zoneSpec azure.ResourceSpecGetter) (managed bool, err error) { + ctx, log, done := tele.StartSpanWithLogger(ctx, "privatedns.Service.deleteZone") + defer done() + + // Skip deleting the private DNS zone when it's not managed by capz. + isManaged, err := s.IsManaged(ctx) + if err != nil { + if azure.ResourceNotFound(err) { + // already deleted or doesn't exist, cleanup status and return. + s.Scope.DeleteLongRunningOperationState(zoneSpec.ResourceName(), serviceName) + return managed, nil + } + return managed, errors.Wrapf(err, "could not get private DNS zone state of %s in resource group %s", zoneSpec.ResourceName(), zoneSpec.ResourceGroupName()) + } + + if !isManaged { + log.V(1).Info("Skipping deletion of unmanaged private DNS zone", "private DNS", zoneSpec.ResourceName()) + return managed, nil + } + + // if we reach here, it means that this vnet link is managed by capz. + managed = true + + // Delete the private DNS zone, which also deletes all records + err = s.zoneReconciler.DeleteResource(ctx, zoneSpec, serviceName) + return managed, err +} diff --git a/azure/services/privatedns/zone_spec.go b/azure/services/privatedns/zone_spec.go new file mode 100644 index 00000000000..30cfd59d37f --- /dev/null +++ b/azure/services/privatedns/zone_spec.go @@ -0,0 +1,69 @@ +/* +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 ( + "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" +) + +// ZoneSpec defines the specification for private dns zone. +type ZoneSpec struct { + Name string + ResourceGroup string + ClusterName string + AdditionalTags infrav1.Tags +} + +// ResourceName returns the name of the private dns zone. +func (s ZoneSpec) ResourceName() string { + return s.Name +} + +// OwnerResourceName is a no-op for private dns zone. +func (s ZoneSpec) OwnerResourceName() string { + return "" +} + +// ResourceGroupName returns the name of the resource group of the private dns zone. +func (s ZoneSpec) ResourceGroupName() string { + return s.ResourceGroup +} + +// Parameters returns the parameters for the private dns zone. +func (s ZoneSpec) Parameters(existing interface{}) (params interface{}, err error) { + if existing != nil { + _, ok := existing.(privatedns.PrivateZone) + if !ok { + return nil, errors.Errorf("%T is not a privatedns.PrivateZone", existing) + } + return nil, nil + } + + return privatedns.PrivateZone{ + Location: to.StringPtr(azure.Global), + Tags: converters.TagsToMap(infrav1.Build(infrav1.BuildParams{ + ClusterName: s.ClusterName, + Lifecycle: infrav1.ResourceLifecycleOwned, + Additional: s.AdditionalTags, + })), + }, nil +} diff --git a/azure/services/privatedns/zone_spec_test.go b/azure/services/privatedns/zone_spec_test.go new file mode 100644 index 00000000000..b5eb2a47d0a --- /dev/null +++ b/azure/services/privatedns/zone_spec_test.go @@ -0,0 +1,117 @@ +/* +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" + "sigs.k8s.io/cluster-api-provider-azure/azure" +) + +var ( + zoneSpec = ZoneSpec{ + Name: "my-zone", + ResourceGroup: "my-rg", + ClusterName: "my-cluster", + AdditionalTags: nil, + } +) + +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(BeNil()) + }, + }, + { + 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) + } + }) + } +} diff --git a/azure/types.go b/azure/types.go index a134ae2615c..ebe3314531e 100644 --- a/azure/types.go +++ b/azure/types.go @@ -82,20 +82,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