diff --git a/api/v1alpha3/azurecluster_conversion.go b/api/v1alpha3/azurecluster_conversion.go index 5ee6d2507b6e..93c4f64e2b99 100644 --- a/api/v1alpha3/azurecluster_conversion.go +++ b/api/v1alpha3/azurecluster_conversion.go @@ -95,6 +95,8 @@ func (src *AzureCluster) ConvertTo(dstRaw conversion.Hub) error { // nolint } } + dst.Status.LongRunningOperationStates = restored.Status.LongRunningOperationStates + return nil } @@ -304,3 +306,15 @@ func Convert_v1alpha3_VnetSpec_To_v1alpha4_VnetSpec(in *VnetSpec, out *infrav1al func Convert_v1alpha4_LoadBalancerSpec_To_v1alpha3_LoadBalancerSpec(in *infrav1alpha4.LoadBalancerSpec, out *LoadBalancerSpec, s apiconversion.Scope) error { return autoConvert_v1alpha4_LoadBalancerSpec_To_v1alpha3_LoadBalancerSpec(in, out, s) } + +// Convert_v1alpha3_Future_To_v1alpha4_Future is an autogenerated conversion function. +func Convert_v1alpha3_Future_To_v1alpha4_Future(in *Future, out *infrav1alpha4.Future, s apiconversion.Scope) error { + out.Data = in.FutureData + return autoConvert_v1alpha3_Future_To_v1alpha4_Future(in, out, s) +} + +// Convert_v1alpha4_Future_To_v1alpha3_Future is an autogenerated conversion function. +func Convert_v1alpha4_Future_To_v1alpha3_Future(in *infrav1alpha4.Future, out *Future, s apiconversion.Scope) error { + out.FutureData = in.Data + return autoConvert_v1alpha4_Future_To_v1alpha3_Future(in, out, s) +} diff --git a/api/v1alpha3/azuremachine_conversion.go b/api/v1alpha3/azuremachine_conversion.go index c42e8ea65875..2119172629df 100644 --- a/api/v1alpha3/azuremachine_conversion.go +++ b/api/v1alpha3/azuremachine_conversion.go @@ -53,6 +53,8 @@ func (src *AzureMachine) ConvertTo(dstRaw conversion.Hub) error { // nolint dst.Spec.SubnetName = restored.Spec.SubnetName + dst.Status.LongRunningOperationStates = restored.Status.LongRunningOperationStates + return nil } diff --git a/api/v1alpha3/zz_generated.conversion.go b/api/v1alpha3/zz_generated.conversion.go index 0fdef6699759..904a328d52e6 100644 --- a/api/v1alpha3/zz_generated.conversion.go +++ b/api/v1alpha3/zz_generated.conversion.go @@ -225,16 +225,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*Future)(nil), (*v1alpha4.Future)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha3_Future_To_v1alpha4_Future(a.(*Future), b.(*v1alpha4.Future), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*v1alpha4.Future)(nil), (*Future)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha4_Future_To_v1alpha3_Future(a.(*v1alpha4.Future), b.(*Future), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*Image)(nil), (*v1alpha4.Image)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha3_Image_To_v1alpha4_Image(a.(*Image), b.(*v1alpha4.Image), scope) }); err != nil { @@ -340,6 +330,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*Future)(nil), (*v1alpha4.Future)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha3_Future_To_v1alpha4_Future(a.(*Future), b.(*v1alpha4.Future), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*IngressRule)(nil), (*v1alpha4.SecurityRule)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha3_IngressRule_To_v1alpha4_SecurityRule(a.(*IngressRule), b.(*v1alpha4.SecurityRule), scope) }); err != nil { @@ -410,6 +405,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1alpha4.Future)(nil), (*Future)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha4_Future_To_v1alpha3_Future(a.(*v1alpha4.Future), b.(*Future), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*v1alpha4.LoadBalancerSpec)(nil), (*LoadBalancerSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha4_LoadBalancerSpec_To_v1alpha3_LoadBalancerSpec(a.(*v1alpha4.LoadBalancerSpec), b.(*LoadBalancerSpec), scope) }); err != nil { @@ -707,6 +707,7 @@ func autoConvert_v1alpha4_AzureClusterStatus_To_v1alpha3_AzureClusterStatus(in * out.FailureDomains = *(*apiv1alpha3.FailureDomains)(unsafe.Pointer(&in.FailureDomains)) out.Ready = in.Ready out.Conditions = *(*apiv1alpha3.Conditions)(unsafe.Pointer(&in.Conditions)) + // WARNING: in.LongRunningOperationStates requires manual conversion: does not exist in peer-type return nil } @@ -884,6 +885,7 @@ func autoConvert_v1alpha4_AzureMachineStatus_To_v1alpha3_AzureMachineStatus(in * out.FailureReason = (*errors.MachineStatusError)(unsafe.Pointer(in.FailureReason)) out.FailureMessage = (*string)(unsafe.Pointer(in.FailureMessage)) out.Conditions = *(*apiv1alpha3.Conditions)(unsafe.Pointer(&in.Conditions)) + // WARNING: in.LongRunningOperationStates requires manual conversion: does not exist in peer-type return nil } @@ -1199,28 +1201,19 @@ func autoConvert_v1alpha3_Future_To_v1alpha4_Future(in *Future, out *v1alpha4.Fu out.Type = in.Type out.ResourceGroup = in.ResourceGroup out.Name = in.Name - out.FutureData = in.FutureData + // WARNING: in.FutureData requires manual conversion: does not exist in peer-type return nil } -// Convert_v1alpha3_Future_To_v1alpha4_Future is an autogenerated conversion function. -func Convert_v1alpha3_Future_To_v1alpha4_Future(in *Future, out *v1alpha4.Future, s conversion.Scope) error { - return autoConvert_v1alpha3_Future_To_v1alpha4_Future(in, out, s) -} - func autoConvert_v1alpha4_Future_To_v1alpha3_Future(in *v1alpha4.Future, out *Future, s conversion.Scope) error { out.Type = in.Type out.ResourceGroup = in.ResourceGroup + // WARNING: in.ServiceName requires manual conversion: does not exist in peer-type out.Name = in.Name - out.FutureData = in.FutureData + // WARNING: in.Data requires manual conversion: does not exist in peer-type return nil } -// Convert_v1alpha4_Future_To_v1alpha3_Future is an autogenerated conversion function. -func Convert_v1alpha4_Future_To_v1alpha3_Future(in *v1alpha4.Future, out *Future, s conversion.Scope) error { - return autoConvert_v1alpha4_Future_To_v1alpha3_Future(in, out, s) -} - func autoConvert_v1alpha3_Image_To_v1alpha4_Image(in *Image, out *v1alpha4.Image, s conversion.Scope) error { out.ID = (*string)(unsafe.Pointer(in.ID)) if in.SharedGallery != nil { diff --git a/api/v1alpha4/azurecluster_types.go b/api/v1alpha4/azurecluster_types.go index f821699ce52d..ee7c42b71d41 100644 --- a/api/v1alpha4/azurecluster_types.go +++ b/api/v1alpha4/azurecluster_types.go @@ -96,11 +96,18 @@ type AzureClusterStatus struct { // Conditions defines current service state of the AzureCluster. // +optional Conditions clusterv1.Conditions `json:"conditions,omitempty"` + + // LongRunningOperationStates saves the states for Azure long-running operations so they can be continued on the + // next reconciliation loop. + // +optional + LongRunningOperationStates Futures `json:"longRunningOperationStates,omitempty"` } // +kubebuilder:object:root=true // +kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".metadata.labels.cluster\\.x-k8s\\.io/cluster-name",description="Cluster to which this AzureCluster belongs" -// +kubebuilder:printcolumn:name="Ready",type="boolean",JSONPath=".status.ready" +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="Reason",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].reason" +// +kubebuilder:printcolumn:name="Message",type="string",priority=1,JSONPath=".status.conditions[?(@.type=='Ready')].message" // +kubebuilder:printcolumn:name="Resource Group",type="string",priority=1,JSONPath=".spec.resourceGroup" // +kubebuilder:printcolumn:name="SubscriptionID",type="string",priority=1,JSONPath=".spec.subscriptionID" // +kubebuilder:printcolumn:name="Location",type="string",priority=1,JSONPath=".spec.location" @@ -137,6 +144,16 @@ func (c *AzureCluster) SetConditions(conditions clusterv1.Conditions) { c.Status.Conditions = conditions } +// GetFutures returns the list of long running operation states for an AzureCluster API object. +func (c *AzureCluster) GetFutures() Futures { + return c.Status.LongRunningOperationStates +} + +// SetFutures will set the given long running operation states on an AzureCluster object. +func (c *AzureCluster) SetFutures(futures Futures) { + c.Status.LongRunningOperationStates = futures +} + func init() { SchemeBuilder.Register(&AzureCluster{}, &AzureClusterList{}) } diff --git a/api/v1alpha4/azuremachine_types.go b/api/v1alpha4/azuremachine_types.go index 5a093044c369..3b2dd02186bf 100644 --- a/api/v1alpha4/azuremachine_types.go +++ b/api/v1alpha4/azuremachine_types.go @@ -175,6 +175,11 @@ type AzureMachineStatus struct { // Conditions defines current service state of the AzureMachine. // +optional Conditions clusterv1.Conditions `json:"conditions,omitempty"` + + // LongRunningOperationStates saves the states for Azure long-running operations so they can be continued on the + // next reconciliation loop. + // +optional + LongRunningOperationStates Futures `json:"longRunningOperationStates,omitempty"` } // +kubebuilder:object:root=true @@ -216,6 +221,16 @@ func (m *AzureMachine) SetConditions(conditions clusterv1.Conditions) { m.Status.Conditions = conditions } +// GetFutures returns the list of long running operation states for an AzureMachine API object. +func (m *AzureMachine) GetFutures() Futures { + return m.Status.LongRunningOperationStates +} + +// SetFutures will set the given long running operation states on an AzureMachine object. +func (m *AzureMachine) SetFutures(futures Futures) { + m.Status.LongRunningOperationStates = futures +} + func init() { SchemeBuilder.Register(&AzureMachine{}, &AzureMachineList{}) } diff --git a/api/v1alpha4/conditions_consts.go b/api/v1alpha4/conditions_consts.go index fa888dbeac22..bdcdf1eecc76 100644 --- a/api/v1alpha4/conditions_consts.go +++ b/api/v1alpha4/conditions_consts.go @@ -20,8 +20,6 @@ import clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4" // AzureCluster Conditions and Reasons. const ( - // NetworkInfrastructureReadyCondition reports of current status of cluster infrastructure. - NetworkInfrastructureReadyCondition clusterv1.ConditionType = "NetworkInfrastructureReady" // NamespaceNotAllowedByIdentity used to indicate cluster in a namespace not allowed by identity. NamespaceNotAllowedByIdentity = "NamespaceNotAllowedByIdentity" ) @@ -75,3 +73,46 @@ const ( // ScaleSetModelOutOfDateReason describes the machine pool model being out of date. ScaleSetModelOutOfDateReason = "ScaleSetModelOutOfDate" ) + +// Azure Services Conditions and Reasons. +const ( + // ResourceGroupReadyCondition means the resource group exists and is ready to be used. + ResourceGroupReadyCondition clusterv1.ConditionType = "ResourceGroupReady" + // VNetReadyCondition means the virtual network exists and is ready to be used. + VNetReadyCondition clusterv1.ConditionType = "VNetReady" + // SecurityGroupsReadyCondition means the security groups exist and are ready to be used. + SecurityGroupsReadyCondition clusterv1.ConditionType = "SecurityGroupsReady" + // RouteTablesReadyCondition means the route tables exist and are ready to be used. + RouteTablesReadyCondition clusterv1.ConditionType = "RouteTablesReady" + // PublicIPsReadyCondition means the public IPs exist and are ready to be used. + PublicIPsReadyCondition clusterv1.ConditionType = "PublicIPsReady" + // NATGatewaysReadyCondition means the NAT gateways exist and are ready to be used. + NATGatewaysReadyCondition clusterv1.ConditionType = "NATGatewaysReady" + // SubnetsReadyCondition means the subnets exist and are ready to be used. + 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" + // 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. + InboundNATRulesReadyCondition clusterv1.ConditionType = "InboundNATRulesReady" + // AvailabilitySetReadyCondition means the availability set exists and is ready to be used. + AvailabilitySetReadyCondition clusterv1.ConditionType = "AvailabilitySetReady" + // RoleAssignmentReadyCondition means the role assignment exists and is ready to be used. + RoleAssignmentReadyCondition clusterv1.ConditionType = "RoleAssignmentReady" + + // CreatingReason means the resource is being created. + CreatingReason = "Creating" + // FailedReason means the resource failed to be created. + FailedReason = "Failed" + // DeletingReason means the resource is being deleted. + DeletingReason = "Deleting" + // DeletedReason means the resource was deleted. + DeletedReason = "Deleted" + // DeletionFailedReason means the resource failed to be deleted. + DeletionFailedReason = "DeletionFailed" + // UpdatingReason means the resource is being updated. + UpdatingReason = "Updating" +) diff --git a/api/v1alpha4/types.go b/api/v1alpha4/types.go index 4475a9169b62..1ea9430e7d45 100644 --- a/api/v1alpha4/types.go +++ b/api/v1alpha4/types.go @@ -29,21 +29,37 @@ const ( Node string = "node" ) +// Futures is a slice of Future. +type Futures []Future + +const ( + // PatchFuture is a future that was derived from a PATCH request. + PatchFuture string = "PATCH" + // PutFuture is a future that was derived from a PUT request. + PutFuture string = "PUT" + // DeleteFuture is a future that was derived from a DELETE request. + DeleteFuture string = "DELETE" +) + // Future contains the data needed for an Azure long-running operation to continue across reconcile loops. type Future struct { - // Type describes the type of future, update, create, delete, etc + // Type describes the type of future, such as update, create, delete, etc. Type string `json:"type"` - // ResourceGroup is the Azure resource group for the resource + // ResourceGroup is the Azure resource group for the resource. // +optional ResourceGroup string `json:"resourceGroup,omitempty"` - // Name is the name of the Azure resource - // +optional - Name string `json:"name,omitempty"` + // ServiceName is the name of the Azure service. + // Together with the name of the resource, this forms the unique identifier for the future. + ServiceName string `json:"serviceName"` + + // Name is the name of the Azure resource. + // Together with the service name, this forms the unique identifier for the future. + Name string `json:"name"` - // FutureData is the base64 url encoded json Azure AutoRest Future - FutureData string `json:"futureData,omitempty"` + // Data is the base64 url encoded json Azure AutoRest Future. + Data string `json:"data,omitempty"` } // NetworkSpec specifies what the Azure networking resources should look like. diff --git a/api/v1alpha4/zz_generated.deepcopy.go b/api/v1alpha4/zz_generated.deepcopy.go index d7899ada440a..030a972ad749 100644 --- a/api/v1alpha4/zz_generated.deepcopy.go +++ b/api/v1alpha4/zz_generated.deepcopy.go @@ -298,6 +298,11 @@ func (in *AzureClusterStatus) DeepCopyInto(out *AzureClusterStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.LongRunningOperationStates != nil { + in, out := &in.LongRunningOperationStates, &out.LongRunningOperationStates + *out = make(Futures, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureClusterStatus. @@ -464,6 +469,11 @@ func (in *AzureMachineStatus) DeepCopyInto(out *AzureMachineStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.LongRunningOperationStates != nil { + in, out := &in.LongRunningOperationStates, &out.LongRunningOperationStates + *out = make(Futures, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureMachineStatus. @@ -801,6 +811,25 @@ func (in *Future) DeepCopy() *Future { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in Futures) DeepCopyInto(out *Futures) { + { + in := &in + *out = make(Futures, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Futures. +func (in Futures) DeepCopy() Futures { + if in == nil { + return nil + } + out := new(Futures) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Image) DeepCopyInto(out *Image) { *out = *in diff --git a/azure/converters/futures.go b/azure/converters/futures.go new file mode 100644 index 000000000000..86d5309f553c --- /dev/null +++ b/azure/converters/futures.go @@ -0,0 +1,54 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package converters + +import ( + "encoding/base64" + + azureautorest "github.com/Azure/go-autorest/autorest/azure" + "github.com/pkg/errors" + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" +) + +// SDKToFuture converts an SDK future to an infrav1.Future. +func SDKToFuture(future azureautorest.FutureAPI, futureType, service, resourceName, rgName string) (*infrav1.Future, error) { + jsonData, err := future.MarshalJSON() + if err != nil { + return nil, errors.Wrap(err, "failed to marshal async future") + } + + return &infrav1.Future{ + Type: futureType, + ResourceGroup: rgName, + ServiceName: service, + Name: resourceName, + Data: base64.URLEncoding.EncodeToString(jsonData), + }, nil +} + +// FutureToSDK converts an infrav1.Future to an SDK future. +func FutureToSDK(future infrav1.Future) (azureautorest.FutureAPI, error) { + futureData, err := base64.URLEncoding.DecodeString(future.Data) + if err != nil { + return nil, errors.Wrap(err, "failed to base64 decode future data") + } + var genericFuture azureautorest.Future + if err := genericFuture.UnmarshalJSON(futureData); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal future data") + } + return &genericFuture, nil +} diff --git a/azure/converters/futures_test.go b/azure/converters/futures_test.go new file mode 100644 index 000000000000..8058be7b215f --- /dev/null +++ b/azure/converters/futures_test.go @@ -0,0 +1,108 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package converters + +import ( + "testing" + + azureautorest "github.com/Azure/go-autorest/autorest/azure" + "github.com/onsi/gomega" + + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" +) + +var ( + validFuture = infrav1.Future{ + Type: infrav1.DeleteFuture, + ServiceName: "test-service", + Name: "test-group", + ResourceGroup: "test-group", + Data: "eyJtZXRob2QiOiJERUxFVEUiLCJwb2xsaW5nTWV0aG9kIjoiTG9jYXRpb24iLCJscm9TdGF0ZSI6IkluUHJvZ3Jlc3MifQ==", + } + + emptyDataFuture = infrav1.Future{ + Type: infrav1.DeleteFuture, + ServiceName: "test-service", + Name: "test-group", + ResourceGroup: "test-group", + Data: "", + } + + decodedDataFuture = infrav1.Future{ + Type: infrav1.DeleteFuture, + ServiceName: "test-service", + Name: "test-group", + ResourceGroup: "test-group", + Data: "this is not b64 encoded", + } + + invalidFuture = infrav1.Future{ + Type: infrav1.DeleteFuture, + ServiceName: "test-service", + Name: "test-group", + ResourceGroup: "test-group", + Data: "ZmFrZSBiNjQgZnV0dXJlIGRhdGEK", + } +) + +func Test_FutureToSDK(t *testing.T) { + cases := []struct { + name string + future infrav1.Future + expect func(*gomega.GomegaWithT, azureautorest.FutureAPI, error) + }{ + { + name: "data is empty", + future: emptyDataFuture, + expect: func(g *gomega.GomegaWithT, f azureautorest.FutureAPI, err error) { + g.Expect(err.Error()).Should(gomega.ContainSubstring("failed to unmarshal future data")) + }, + }, + { + name: "data is not base64 encoded", + future: decodedDataFuture, + expect: func(g *gomega.GomegaWithT, f azureautorest.FutureAPI, err error) { + g.Expect(err.Error()).Should(gomega.ContainSubstring("failed to base64 decode future data")) + }, + }, + { + name: "base64 data is not a valid future", + future: invalidFuture, + expect: func(g *gomega.GomegaWithT, f azureautorest.FutureAPI, err error) { + g.Expect(err.Error()).Should(gomega.ContainSubstring("failed to unmarshal future data")) + }, + }, + { + name: "valid future data", + future: validFuture, + expect: func(g *gomega.GomegaWithT, f azureautorest.FutureAPI, err error) { + g.Expect(err).Should(gomega.BeNil()) + g.Expect(f).Should(gomega.BeAssignableToTypeOf(&azureautorest.Future{})) + }, + }, + } + + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + t.Parallel() + g := gomega.NewGomegaWithT(t) + result, err := FutureToSDK(c.future) + c.expect(g, result, err) + }) + } +} diff --git a/azure/defaults.go b/azure/defaults.go index fb2d439e8c77..8643212cd7e8 100644 --- a/azure/defaults.go +++ b/azure/defaults.go @@ -328,6 +328,13 @@ func SetAutoRestClientDefaults(c *autorest.Client, auth autorest.Authorizer) { // The wrapped Sender should set the x-ms-correlation-request-id on the given // request, then pass the new request to the underlying Sender. c.Sender = autorest.DecorateSender(c.Sender, msCorrelationIDSendDecorator) + // The default number of retries is 3. This means the client will attempt to retry operation results like resource + // conflicts (HTTP 409). For a reconciling controller, this is undesirable behavior since if the controller runs + // into an error reconciling, the controller would be better off to end with an error and try again later. + // + // Unfortunately, the naming of this field is a bit misleading. This is not actually "retry attempts", it actually + // is attempts. Setting this to a value of 0 will cause a panic in Go AutoRest. + c.RetryAttempts = 1 AutoRestClientAppendUserAgent(c, UserAgent()) } diff --git a/azure/errors.go b/azure/errors.go index 56360487e01b..f5474d393f65 100644 --- a/azure/errors.go +++ b/azure/errors.go @@ -107,8 +107,8 @@ func (t ReconcileError) IsTerminal() bool { return t.errorType == TerminalErrorType } -// Is returns true if the target is a ReconcileError. -func (t ReconcileError) Is(target error) bool { +// IsReconcileError returns true if the target is a ReconcileError. +func IsReconcileError(target error) bool { return errors.As(target, &ReconcileError{}) } @@ -133,8 +133,8 @@ type OperationNotDoneError struct { } // NewOperationNotDoneError returns a new OperationNotDoneError wrapping a Future. -func NewOperationNotDoneError(future *infrav1.Future) *OperationNotDoneError { - return &OperationNotDoneError{ +func NewOperationNotDoneError(future *infrav1.Future) OperationNotDoneError { + return OperationNotDoneError{ Future: future, } } @@ -144,7 +144,11 @@ func (onde OperationNotDoneError) Error() string { return fmt.Sprintf("operation type %s on Azure resource %s/%s is not done", onde.Future.Type, onde.Future.ResourceGroup, onde.Future.Name) } -// Is returns true if the target is an OperationNotDoneError. -func (onde OperationNotDoneError) Is(target error) bool { +// IsOperationNotDoneError returns true if the target is an OperationNotDoneError. +func IsOperationNotDoneError(target error) bool { + reconcileErr := &ReconcileError{} + if errors.As(target, reconcileErr) { + return IsOperationNotDoneError(reconcileErr.error) + } return errors.As(target, &OperationNotDoneError{}) } diff --git a/azure/interfaces.go b/azure/interfaces.go index 6001f0c9c858..7b7ceea28ba5 100644 --- a/azure/interfaces.go +++ b/azure/interfaces.go @@ -22,6 +22,7 @@ import ( "github.com/Azure/go-autorest/autorest" infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" + clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4" ) // Reconciler is a generic interface used by components offering a type of service. @@ -80,8 +81,33 @@ type ClusterDescriber interface { CloudProviderConfigOverrides() *infrav1.CloudProviderConfigOverrides } +// AsyncStatusUpdater is an interface used to keep track of long running operations in Status that has Conditions and Futures. +type AsyncStatusUpdater interface { + SetLongRunningOperationState(*infrav1.Future) + GetLongRunningOperationState(string, string) *infrav1.Future + DeleteLongRunningOperationState(string, string) + UpdatePutStatus(clusterv1.ConditionType, string, error) + UpdateDeleteStatus(clusterv1.ConditionType, string, error) + UpdatePatchStatus(clusterv1.ConditionType, string, error) +} + // ClusterScoper combines the ClusterDescriber and NetworkDescriber interfaces. type ClusterScoper interface { ClusterDescriber NetworkDescriber } + +// ResourceSpecGetter is an interface for getting all the required information to create/update/delete an Azure resource. +type ResourceSpecGetter interface { + // ResourceName returns the name of the resource. + ResourceName() string + // OwnerResourceName returns the name of the resource that owns the resource + // in the case that the resource is an Azure subresource. + OwnerResourceName() string + // ResourceGroupName returns the name of the resource group the resource is in. + ResourceGroupName() string + // Parameters takes the existing resource and returns the desired parameters of the resource. + // If the resource does not exist, or we do not care about existing parameters to update the resource, existing should be nil. + // If no update is needed on the resource, Parameters should return nil. + Parameters(existing interface{}) (interface{}, error) +} diff --git a/azure/mocks/service_mock.go b/azure/mock_azure/azure_mock.go similarity index 84% rename from azure/mocks/service_mock.go rename to azure/mock_azure/azure_mock.go index fec8422ec579..408e24f4b7ec 100644 --- a/azure/mocks/service_mock.go +++ b/azure/mock_azure/azure_mock.go @@ -17,8 +17,8 @@ limitations under the License. // Code generated by MockGen. DO NOT EDIT. // Source: ../interfaces.go -// Package mocks is a generated GoMock package. -package mocks +// Package mock_azure is a generated GoMock package. +package mock_azure import ( context "context" @@ -27,6 +27,7 @@ import ( autorest "github.com/Azure/go-autorest/autorest" gomock "github.com/golang/mock/gomock" v1alpha4 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" + v1alpha40 "sigs.k8s.io/cluster-api/api/v1alpha4" ) // MockReconciler is a mock of Reconciler interface. @@ -731,6 +732,103 @@ func (mr *MockClusterDescriberMockRecorder) TenantID() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TenantID", reflect.TypeOf((*MockClusterDescriber)(nil).TenantID)) } +// MockAsyncStatusUpdater is a mock of AsyncStatusUpdater interface. +type MockAsyncStatusUpdater struct { + ctrl *gomock.Controller + recorder *MockAsyncStatusUpdaterMockRecorder +} + +// MockAsyncStatusUpdaterMockRecorder is the mock recorder for MockAsyncStatusUpdater. +type MockAsyncStatusUpdaterMockRecorder struct { + mock *MockAsyncStatusUpdater +} + +// NewMockAsyncStatusUpdater creates a new mock instance. +func NewMockAsyncStatusUpdater(ctrl *gomock.Controller) *MockAsyncStatusUpdater { + mock := &MockAsyncStatusUpdater{ctrl: ctrl} + mock.recorder = &MockAsyncStatusUpdaterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAsyncStatusUpdater) EXPECT() *MockAsyncStatusUpdaterMockRecorder { + return m.recorder +} + +// DeleteLongRunningOperationState mocks base method. +func (m *MockAsyncStatusUpdater) DeleteLongRunningOperationState(arg0, arg1 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "DeleteLongRunningOperationState", arg0, arg1) +} + +// DeleteLongRunningOperationState indicates an expected call of DeleteLongRunningOperationState. +func (mr *MockAsyncStatusUpdaterMockRecorder) DeleteLongRunningOperationState(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLongRunningOperationState", reflect.TypeOf((*MockAsyncStatusUpdater)(nil).DeleteLongRunningOperationState), arg0, arg1) +} + +// GetLongRunningOperationState mocks base method. +func (m *MockAsyncStatusUpdater) GetLongRunningOperationState(arg0, arg1 string) *v1alpha4.Future { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLongRunningOperationState", arg0, arg1) + ret0, _ := ret[0].(*v1alpha4.Future) + return ret0 +} + +// GetLongRunningOperationState indicates an expected call of GetLongRunningOperationState. +func (mr *MockAsyncStatusUpdaterMockRecorder) GetLongRunningOperationState(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLongRunningOperationState", reflect.TypeOf((*MockAsyncStatusUpdater)(nil).GetLongRunningOperationState), arg0, arg1) +} + +// SetLongRunningOperationState mocks base method. +func (m *MockAsyncStatusUpdater) SetLongRunningOperationState(arg0 *v1alpha4.Future) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetLongRunningOperationState", arg0) +} + +// SetLongRunningOperationState indicates an expected call of SetLongRunningOperationState. +func (mr *MockAsyncStatusUpdaterMockRecorder) SetLongRunningOperationState(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLongRunningOperationState", reflect.TypeOf((*MockAsyncStatusUpdater)(nil).SetLongRunningOperationState), arg0) +} + +// UpdateDeleteStatus mocks base method. +func (m *MockAsyncStatusUpdater) UpdateDeleteStatus(arg0 v1alpha40.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 *MockAsyncStatusUpdaterMockRecorder) UpdateDeleteStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeleteStatus", reflect.TypeOf((*MockAsyncStatusUpdater)(nil).UpdateDeleteStatus), arg0, arg1, arg2) +} + +// UpdatePatchStatus mocks base method. +func (m *MockAsyncStatusUpdater) UpdatePatchStatus(arg0 v1alpha40.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 *MockAsyncStatusUpdaterMockRecorder) UpdatePatchStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePatchStatus", reflect.TypeOf((*MockAsyncStatusUpdater)(nil).UpdatePatchStatus), arg0, arg1, arg2) +} + +// UpdatePutStatus mocks base method. +func (m *MockAsyncStatusUpdater) UpdatePutStatus(arg0 v1alpha40.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 *MockAsyncStatusUpdaterMockRecorder) UpdatePutStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePutStatus", reflect.TypeOf((*MockAsyncStatusUpdater)(nil).UpdatePutStatus), arg0, arg1, arg2) +} + // MockClusterScoper is a mock of ClusterScoper interface. type MockClusterScoper struct { ctrl *gomock.Controller @@ -1157,3 +1255,83 @@ func (mr *MockClusterScoperMockRecorder) Vnet() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Vnet", reflect.TypeOf((*MockClusterScoper)(nil).Vnet)) } + +// MockResourceSpecGetter is a mock of ResourceSpecGetter interface. +type MockResourceSpecGetter struct { + ctrl *gomock.Controller + recorder *MockResourceSpecGetterMockRecorder +} + +// MockResourceSpecGetterMockRecorder is the mock recorder for MockResourceSpecGetter. +type MockResourceSpecGetterMockRecorder struct { + mock *MockResourceSpecGetter +} + +// NewMockResourceSpecGetter creates a new mock instance. +func NewMockResourceSpecGetter(ctrl *gomock.Controller) *MockResourceSpecGetter { + mock := &MockResourceSpecGetter{ctrl: ctrl} + mock.recorder = &MockResourceSpecGetterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockResourceSpecGetter) EXPECT() *MockResourceSpecGetterMockRecorder { + return m.recorder +} + +// OwnerResourceName mocks base method. +func (m *MockResourceSpecGetter) OwnerResourceName() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OwnerResourceName") + ret0, _ := ret[0].(string) + return ret0 +} + +// OwnerResourceName indicates an expected call of OwnerResourceName. +func (mr *MockResourceSpecGetterMockRecorder) OwnerResourceName() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OwnerResourceName", reflect.TypeOf((*MockResourceSpecGetter)(nil).OwnerResourceName)) +} + +// Parameters mocks base method. +func (m *MockResourceSpecGetter) Parameters(existing interface{}) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Parameters", existing) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Parameters indicates an expected call of Parameters. +func (mr *MockResourceSpecGetterMockRecorder) Parameters(existing interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Parameters", reflect.TypeOf((*MockResourceSpecGetter)(nil).Parameters), existing) +} + +// ResourceGroupName mocks base method. +func (m *MockResourceSpecGetter) ResourceGroupName() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ResourceGroupName") + ret0, _ := ret[0].(string) + return ret0 +} + +// ResourceGroupName indicates an expected call of ResourceGroupName. +func (mr *MockResourceSpecGetterMockRecorder) ResourceGroupName() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResourceGroupName", reflect.TypeOf((*MockResourceSpecGetter)(nil).ResourceGroupName)) +} + +// ResourceName mocks base method. +func (m *MockResourceSpecGetter) ResourceName() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ResourceName") + ret0, _ := ret[0].(string) + return ret0 +} + +// ResourceName indicates an expected call of ResourceName. +func (mr *MockResourceSpecGetterMockRecorder) ResourceName() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResourceName", reflect.TypeOf((*MockResourceSpecGetter)(nil).ResourceName)) +} diff --git a/azure/mocks/doc.go b/azure/mock_azure/doc.go similarity index 73% rename from azure/mocks/doc.go rename to azure/mock_azure/doc.go index 92f6e009db0a..baf8d4d9ac45 100644 --- a/azure/mocks/doc.go +++ b/azure/mock_azure/doc.go @@ -15,6 +15,6 @@ limitations under the License. */ // Run go generate to regenerate this mock. -//go:generate ../../hack/tools/bin/mockgen -destination service_mock.go -package mocks -source ../interfaces.go Service -//go:generate /usr/bin/env bash -c "cat ../../hack/boilerplate/boilerplate.generatego.txt service_mock.go > _service_mock.go && mv _service_mock.go service_mock.go" -package mocks //nolint +//go:generate ../../hack/tools/bin/mockgen -destination azure_mock.go -package mock_azure -source ../interfaces.go +//go:generate /usr/bin/env bash -c "cat ../../hack/boilerplate/boilerplate.generatego.txt azure_mock.go > _azure_mock.go && mv _azure_mock.go azure_mock.go" +package mock_azure //nolint diff --git a/azure/scope/cluster.go b/azure/scope/cluster.go index ed8337419879..56b6fce782aa 100644 --- a/azure/scope/cluster.go +++ b/azure/scope/cluster.go @@ -36,6 +36,7 @@ import ( infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" "sigs.k8s.io/cluster-api-provider-azure/azure" + "sigs.k8s.io/cluster-api-provider-azure/util/futures" ) // ClusterScopeParams defines the input parameters used to create a new Scope. @@ -538,21 +539,13 @@ func (s *ClusterScope) ListOptionsLabelSelector() client.ListOption { // PatchObject persists the cluster configuration and status. func (s *ClusterScope) PatchObject(ctx context.Context) error { - conditions.SetSummary(s.AzureCluster, - conditions.WithConditions( - infrav1.NetworkInfrastructureReadyCondition, - ), - conditions.WithStepCounterIfOnly( - infrav1.NetworkInfrastructureReadyCondition, - ), - ) + conditions.SetSummary(s.AzureCluster) return s.patchHelper.Patch( ctx, s.AzureCluster, patch.WithOwnedConditions{Conditions: []clusterv1.ConditionType{ clusterv1.ReadyCondition, - infrav1.NetworkInfrastructureReadyCondition, }}) } @@ -678,3 +671,61 @@ func (s *ClusterScope) getOutboundLBPublicIPSpecs(outboundLB *infrav1.LoadBalanc return outboundIPSpecs } + +// SetLongRunningOperationState will set the future on the AzureCluster status to allow the resource to continue +// in the next reconciliation. +func (s *ClusterScope) SetLongRunningOperationState(future *infrav1.Future) { + futures.Set(s.AzureCluster, future) +} + +// GetLongRunningOperationState will get the future on the AzureCluster status. +func (s *ClusterScope) GetLongRunningOperationState(name, service string) *infrav1.Future { + return futures.Get(s.AzureCluster, name, service) +} + +// DeleteLongRunningOperationState will delete the future from the AzureCluster status. +func (s *ClusterScope) DeleteLongRunningOperationState(name, service string) { + futures.Delete(s.AzureCluster, name, service) +} + +// UpdateDeleteStatus updates a condition on the AzureCluster status after a DELETE operation. +func (s *ClusterScope) UpdateDeleteStatus(condition clusterv1.ConditionType, service string, err error) { + switch { + case err == nil: + conditions.MarkFalse(s.AzureCluster, condition, infrav1.DeletedReason, clusterv1.ConditionSeverityInfo, "%s successfully deleted", service) + case errors.Is(err, azure.ErrNotOwned): + // do nothing + case azure.IsOperationNotDoneError(err): + conditions.MarkFalse(s.AzureCluster, condition, infrav1.DeletingReason, clusterv1.ConditionSeverityInfo, "%s deleting", service) + default: + conditions.MarkFalse(s.AzureCluster, condition, infrav1.DeletionFailedReason, clusterv1.ConditionSeverityError, "%s failed to delete. err: %s", service, err.Error()) + } +} + +// UpdatePutStatus updates a condition on the AzureCluster status after a PUT operation. +func (s *ClusterScope) UpdatePutStatus(condition clusterv1.ConditionType, service string, err error) { + switch { + case err == nil: + conditions.MarkTrue(s.AzureCluster, condition) + case errors.Is(err, azure.ErrNotOwned): + // do nothing + case azure.IsOperationNotDoneError(err): + conditions.MarkFalse(s.AzureCluster, condition, infrav1.CreatingReason, clusterv1.ConditionSeverityInfo, "%s creating or updating", service) + default: + conditions.MarkFalse(s.AzureCluster, condition, infrav1.FailedReason, clusterv1.ConditionSeverityError, "%s failed to create or update. err: %s", service, err.Error()) + } +} + +// UpdatePatchStatus updates a condition on the AzureCluster status after a PATCH operation. +func (s *ClusterScope) UpdatePatchStatus(condition clusterv1.ConditionType, service string, err error) { + switch { + case err == nil: + conditions.MarkTrue(s.AzureCluster, condition) + case errors.Is(err, azure.ErrNotOwned): + // do nothing + case azure.IsOperationNotDoneError(err): + conditions.MarkFalse(s.AzureCluster, condition, infrav1.UpdatingReason, clusterv1.ConditionSeverityInfo, "%s updating", service) + default: + conditions.MarkFalse(s.AzureCluster, condition, infrav1.FailedReason, clusterv1.ConditionSeverityError, "%s failed to update. err: %s", service, err.Error()) + } +} diff --git a/azure/scope/machine.go b/azure/scope/machine.go index 8f71d522df14..4987fccb22e4 100644 --- a/azure/scope/machine.go +++ b/azure/scope/machine.go @@ -39,6 +39,7 @@ import ( infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" "sigs.k8s.io/cluster-api-provider-azure/azure" + "sigs.k8s.io/cluster-api-provider-azure/util/futures" ) // MachineScopeParams defines the input parameters used to create a new MachineScope. @@ -561,3 +562,61 @@ func (m *MachineScope) SetSubnetName() error { return nil } + +// SetLongRunningOperationState will set the future on the AzureMachine status to allow the resource to continue +// in the next reconciliation. +func (m *MachineScope) SetLongRunningOperationState(future *infrav1.Future) { + futures.Set(m.AzureMachine, future) +} + +// GetLongRunningOperationState will get the future on the AzureMachine status. +func (m *MachineScope) GetLongRunningOperationState(name, service string) *infrav1.Future { + return futures.Get(m.AzureMachine, name, service) +} + +// DeleteLongRunningOperationState will delete the future from the AzureMachine status. +func (m *MachineScope) DeleteLongRunningOperationState(name, service string) { + futures.Delete(m.AzureMachine, name, service) +} + +// UpdateDeleteStatus updates a condition on the AzureMachine status after a DELETE operation. +func (m *MachineScope) UpdateDeleteStatus(condition clusterv1.ConditionType, service string, err error) { + switch { + case err == nil: + conditions.MarkFalse(m.AzureMachine, condition, infrav1.DeletedReason, clusterv1.ConditionSeverityInfo, "%s successfully deleted", service) + case errors.Is(err, azure.ErrNotOwned): + // do nothing + case azure.IsOperationNotDoneError(err): + conditions.MarkFalse(m.AzureMachine, condition, infrav1.DeletingReason, clusterv1.ConditionSeverityInfo, "%s deleting", service) + default: + conditions.MarkFalse(m.AzureMachine, condition, infrav1.DeletionFailedReason, clusterv1.ConditionSeverityError, "%s failed to delete. err: %s", service, err.Error()) + } +} + +// UpdatePutStatus updates a condition on the AzureMachine status after a PUT operation. +func (m *MachineScope) UpdatePutStatus(condition clusterv1.ConditionType, service string, err error) { + switch { + case err == nil: + conditions.MarkTrue(m.AzureMachine, condition) + case errors.Is(err, azure.ErrNotOwned): + // do nothing + case azure.IsOperationNotDoneError(err): + conditions.MarkFalse(m.AzureMachine, condition, infrav1.CreatingReason, clusterv1.ConditionSeverityInfo, "%s creating or updating", service) + default: + conditions.MarkFalse(m.AzureMachine, condition, infrav1.FailedReason, clusterv1.ConditionSeverityError, "%s failed to create or update. err: %s", service, err.Error()) + } +} + +// UpdatePatchStatus updates a condition on the AzureMachine status after a PATCH operation. +func (m *MachineScope) UpdatePatchStatus(condition clusterv1.ConditionType, service string, err error) { + switch { + case err == nil: + conditions.MarkTrue(m.AzureMachine, condition) + case errors.Is(err, azure.ErrNotOwned): + // do nothing + case azure.IsOperationNotDoneError(err): + conditions.MarkFalse(m.AzureMachine, condition, infrav1.UpdatingReason, clusterv1.ConditionSeverityInfo, "%s updating", service) + default: + conditions.MarkFalse(m.AzureMachine, condition, infrav1.FailedReason, clusterv1.ConditionSeverityError, "%s failed to update. err: %s", service, err.Error()) + } +} diff --git a/azure/scope/machinepool.go b/azure/scope/machinepool.go index 71208b99822e..97e5269f1323 100644 --- a/azure/scope/machinepool.go +++ b/azure/scope/machinepool.go @@ -22,6 +22,8 @@ import ( "strings" "time" + "sigs.k8s.io/cluster-api-provider-azure/util/futures" + "github.com/Azure/go-autorest/autorest/to" "github.com/go-logr/logr" "github.com/pkg/errors" @@ -45,6 +47,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) +// ScalesetsServiceName is the name of the scalesets service. +// TODO: move this to scalesets.go once we remove the usage in this package, +// added here to avoid a circular dependency. +const ScalesetsServiceName = "scalesets" + type ( // MachinePoolScopeParams defines the input parameters used to create a new MachinePoolScope. MachinePoolScopeParams struct { @@ -297,7 +304,7 @@ func (m *MachinePoolScope) applyAzureMachinePoolMachines(ctx context.Context) er return nil } - if m.GetLongRunningOperationState() != nil { + if futures.Has(m.AzureMachinePool, m.Name(), ScalesetsServiceName) { m.V(4).Info("exiting early due an in-progress long running operation on the ScaleSet") // exit early to be less greedy about delete return nil @@ -373,13 +380,17 @@ func (m *MachinePoolScope) createMachine(ctx context.Context, machine azure.VMSS // SetLongRunningOperationState will set the future on the AzureMachinePool status to allow the resource to continue // in the next reconciliation. func (m *MachinePoolScope) SetLongRunningOperationState(future *infrav1.Future) { - m.AzureMachinePool.Status.LongRunningOperationState = future + futures.Set(m.AzureMachinePool, future) } -// GetLongRunningOperationState will get the future on the AzureMachinePool status to allow the resource to continue -// in the next reconciliation. -func (m *MachinePoolScope) GetLongRunningOperationState() *infrav1.Future { - return m.AzureMachinePool.Status.LongRunningOperationState +// GetLongRunningOperationState will get the future on the AzureMachinePool status. +func (m *MachinePoolScope) GetLongRunningOperationState(name, service string) *infrav1.Future { + return futures.Get(m.AzureMachinePool, name, service) +} + +// DeleteLongRunningOperationState will delete the future from the AzureMachinePool status. +func (m *MachinePoolScope) DeleteLongRunningOperationState(name, service string) { + futures.Delete(m.AzureMachinePool, name, service) } // setProvisioningStateAndConditions sets the AzureMachinePool provisioning state and conditions. @@ -618,3 +629,45 @@ func (m *MachinePoolScope) SetSubnetName() error { return nil } + +// UpdateDeleteStatus updates a condition on the AzureMachinePool status after a DELETE operation. +func (m *MachinePoolScope) UpdateDeleteStatus(condition clusterv1.ConditionType, service string, err error) { + switch { + case err == nil: + conditions.MarkFalse(m.AzureMachinePool, condition, infrav1.DeletedReason, clusterv1.ConditionSeverityInfo, "%s successfully deleted", service) + case errors.Is(err, azure.ErrNotOwned): + // do nothing + case azure.IsOperationNotDoneError(err): + conditions.MarkFalse(m.AzureMachinePool, condition, infrav1.DeletingReason, clusterv1.ConditionSeverityInfo, "%s deleting", service) + default: + conditions.MarkFalse(m.AzureMachinePool, condition, infrav1.DeletionFailedReason, clusterv1.ConditionSeverityError, "%s failed to delete. err: %s", service, err.Error()) + } +} + +// UpdatePutStatus updates a condition on the AzureMachinePool status after a PUT operation. +func (m *MachinePoolScope) UpdatePutStatus(condition clusterv1.ConditionType, service string, err error) { + switch { + case err == nil: + conditions.MarkTrue(m.AzureMachinePool, condition) + case errors.Is(err, azure.ErrNotOwned): + // do nothing + case azure.IsOperationNotDoneError(err): + conditions.MarkFalse(m.AzureMachinePool, condition, infrav1.CreatingReason, clusterv1.ConditionSeverityInfo, "%s creating or updating", service) + default: + conditions.MarkFalse(m.AzureMachinePool, condition, infrav1.FailedReason, clusterv1.ConditionSeverityError, "%s failed to create or update. err: %s", service, err.Error()) + } +} + +// UpdatePatchStatus updates a condition on the AzureMachinePool status after a PATCH operation. +func (m *MachinePoolScope) UpdatePatchStatus(condition clusterv1.ConditionType, service string, err error) { + switch { + case err == nil: + conditions.MarkTrue(m.AzureMachinePool, condition) + case errors.Is(err, azure.ErrNotOwned): + // do nothing + case azure.IsOperationNotDoneError(err): + conditions.MarkFalse(m.AzureMachinePool, condition, infrav1.UpdatingReason, clusterv1.ConditionSeverityInfo, "%s updating", service) + default: + conditions.MarkFalse(m.AzureMachinePool, condition, infrav1.FailedReason, clusterv1.ConditionSeverityError, "%s failed to update. err: %s", service, err.Error()) + } +} diff --git a/azure/scope/machinepoolmachine.go b/azure/scope/machinepoolmachine.go index f29ab7373e14..0a1cddd10359 100644 --- a/azure/scope/machinepoolmachine.go +++ b/azure/scope/machinepoolmachine.go @@ -43,6 +43,7 @@ import ( infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" "sigs.k8s.io/cluster-api-provider-azure/azure" infrav1exp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1alpha4" + "sigs.k8s.io/cluster-api-provider-azure/util/futures" "sigs.k8s.io/cluster-api-provider-azure/util/tele" ) @@ -173,14 +174,62 @@ func (s *MachinePoolMachineScope) ScaleSetName() string { return s.MachinePoolScope.Name() } -// GetLongRunningOperationState gets a future representing the current state of a long-running operation if one exists. -func (s *MachinePoolMachineScope) GetLongRunningOperationState() *infrav1.Future { - return s.AzureMachinePoolMachine.Status.LongRunningOperationState +// SetLongRunningOperationState will set the future on the AzureMachinePoolMachine status to allow the resource to continue +// in the next reconciliation. +func (s *MachinePoolMachineScope) SetLongRunningOperationState(future *infrav1.Future) { + futures.Set(s.AzureMachinePoolMachine, future) } -// SetLongRunningOperationState sets a future representing the current state of a long-running operation. -func (s *MachinePoolMachineScope) SetLongRunningOperationState(future *infrav1.Future) { - s.AzureMachinePoolMachine.Status.LongRunningOperationState = future +// GetLongRunningOperationState will get the future on the AzureMachinePoolMachine status. +func (s *MachinePoolMachineScope) GetLongRunningOperationState(name, service string) *infrav1.Future { + return futures.Get(s.AzureMachinePoolMachine, name, service) +} + +// DeleteLongRunningOperationState will delete the future from the AzureMachinePoolMachine status. +func (s *MachinePoolMachineScope) DeleteLongRunningOperationState(name, service string) { + futures.Delete(s.AzureMachinePoolMachine, name, service) +} + +// UpdateDeleteStatus updates a condition on the AzureMachinePoolMachine status after a DELETE operation. +func (s *MachinePoolMachineScope) UpdateDeleteStatus(condition clusterv1.ConditionType, service string, err error) { + switch { + case err == nil: + conditions.MarkFalse(s.AzureMachinePoolMachine, condition, infrav1.DeletedReason, clusterv1.ConditionSeverityInfo, "%s successfully deleted", service) + case errors.Is(err, azure.ErrNotOwned): + // do nothing + case azure.IsOperationNotDoneError(err): + conditions.MarkFalse(s.AzureMachinePoolMachine, condition, infrav1.DeletingReason, clusterv1.ConditionSeverityInfo, "%s deleting", service) + default: + conditions.MarkFalse(s.AzureMachinePoolMachine, condition, infrav1.DeletionFailedReason, clusterv1.ConditionSeverityError, "%s failed to delete. err: %s", service, err.Error()) + } +} + +// UpdatePutStatus updates a condition on the AzureMachinePoolMachine status after a PUT operation. +func (s *MachinePoolMachineScope) UpdatePutStatus(condition clusterv1.ConditionType, service string, err error) { + switch { + case err == nil: + conditions.MarkTrue(s.AzureMachinePoolMachine, condition) + case errors.Is(err, azure.ErrNotOwned): + // do nothing + case azure.IsOperationNotDoneError(err): + conditions.MarkFalse(s.AzureMachinePoolMachine, condition, infrav1.CreatingReason, clusterv1.ConditionSeverityInfo, "%s creating or updating", service) + default: + conditions.MarkFalse(s.AzureMachinePoolMachine, condition, infrav1.FailedReason, clusterv1.ConditionSeverityError, "%s failed to create or update. err: %s", service, err.Error()) + } +} + +// UpdatePatchStatus updates a condition on the AzureMachinePoolMachine status after a PATCH operation. +func (s *MachinePoolMachineScope) UpdatePatchStatus(condition clusterv1.ConditionType, service string, err error) { + switch { + case err == nil: + conditions.MarkTrue(s.AzureMachinePoolMachine, condition) + case errors.Is(err, azure.ErrNotOwned): + // do nothing + case azure.IsOperationNotDoneError(err): + conditions.MarkFalse(s.AzureMachinePoolMachine, condition, infrav1.UpdatingReason, clusterv1.ConditionSeverityInfo, "%s updating", service) + default: + conditions.MarkFalse(s.AzureMachinePoolMachine, condition, infrav1.FailedReason, clusterv1.ConditionSeverityError, "%s failed to update. err: %s", service, err.Error()) + } } // SetVMSSVM update the scope with the current state of the VMSS VM. diff --git a/azure/scope/managedcontrolplane.go b/azure/scope/managedcontrolplane.go index ebd4b90e1dc7..13b93bdf62bd 100644 --- a/azure/scope/managedcontrolplane.go +++ b/azure/scope/managedcontrolplane.go @@ -39,6 +39,7 @@ import ( infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" "sigs.k8s.io/cluster-api-provider-azure/azure" infrav1exp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1alpha4" + "sigs.k8s.io/cluster-api-provider-azure/util/futures" ) // ManagedControlPlaneScopeParams defines the input parameters used to create a new managed @@ -544,3 +545,34 @@ func (s *ManagedControlPlaneScope) GetKubeConfigData() []byte { func (s *ManagedControlPlaneScope) SetKubeConfigData(kubeConfigData []byte) { s.kubeConfigData = kubeConfigData } + +// SetLongRunningOperationState will set the future on the AzureManagedControlPlane status to allow the resource to continue +// in the next reconciliation. +func (s *ManagedControlPlaneScope) SetLongRunningOperationState(future *infrav1.Future) { + futures.Set(s.ControlPlane, future) +} + +// GetLongRunningOperationState will get the future on the AzureManagedControlPlane status. +func (s *ManagedControlPlaneScope) GetLongRunningOperationState(name, service string) *infrav1.Future { + return futures.Get(s.ControlPlane, name, service) +} + +// DeleteLongRunningOperationState will delete the future from the AzureManagedControlPlane status. +func (s *ManagedControlPlaneScope) DeleteLongRunningOperationState(name, service string) { + futures.Delete(s.ControlPlane, name, service) +} + +// UpdateDeleteStatus updates a condition on the AzureManagedControlPlane status after a DELETE operation. +func (s *ManagedControlPlaneScope) UpdateDeleteStatus(condition clusterv1.ConditionType, service string, err error) { + // TODO: add condition to AzureManagedControlPlane status +} + +// UpdatePutStatus updates a condition on the AzureManagedControlPlane status after a PUT operation. +func (s *ManagedControlPlaneScope) UpdatePutStatus(condition clusterv1.ConditionType, service string, err error) { + // TODO: add condition to AzureManagedControlPlane status +} + +// UpdatePatchStatus updates a condition on the AzureManagedControlPlane status after a PATCH operation. +func (s *ManagedControlPlaneScope) UpdatePatchStatus(condition clusterv1.ConditionType, service string, err error) { + // TODO: add condition to AzureManagedControlPlane status +} diff --git a/azure/services/async/helper.go b/azure/services/async/helper.go new file mode 100644 index 000000000000..a4f55d1e90d6 --- /dev/null +++ b/azure/services/async/helper.go @@ -0,0 +1,123 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package async + +import ( + "context" + + "github.com/pkg/errors" + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" + "sigs.k8s.io/cluster-api-provider-azure/azure" + "sigs.k8s.io/cluster-api-provider-azure/azure/converters" + "sigs.k8s.io/cluster-api-provider-azure/util/reconciler" +) + +// ProcessOngoingOperation is a helper function that will process an ongoing operation to check if it is done. +// If it is not done, it will return a transient error. +func ProcessOngoingOperation(ctx context.Context, scope FutureScope, client FutureHandler, resourceName string, serviceName string) error { + future := scope.GetLongRunningOperationState(resourceName, serviceName) + if future == nil { + scope.V(2).Info("no long running operation found", "service", serviceName, "resource", resourceName) + return nil + } + sdkFuture, err := converters.FutureToSDK(*future) + if err != nil { + // Reset the future data to avoid getting stuck in a bad loop. + scope.DeleteLongRunningOperationState(resourceName, serviceName) + return errors.Wrap(err, "could not decode future data, resetting long-running operation state") + } + done, err := client.IsDone(ctx, sdkFuture) + if err != nil { + return errors.Wrap(err, "failed checking if the operation was complete") + } + + if !done { + // Operation is still in progress, update conditions and requeue. + scope.V(2).Info("long running operation is still ongoing", "service", serviceName, "resource", resourceName) + return azure.WithTransientError(azure.NewOperationNotDoneError(future), reconciler.DefaultReconcilerRequeue) + } + + // Resource has been created/deleted/updated. + scope.V(2).Info("long running operation has completed", "service", serviceName, "resource", resourceName) + scope.DeleteLongRunningOperationState(resourceName, serviceName) + return nil +} + +// CreateResource implements the logic for creating a resource Asynchronously. +func CreateResource(ctx context.Context, scope FutureScope, client Creator, spec azure.ResourceSpecGetter, serviceName string) error { + resourceName := spec.ResourceName() + rgName := spec.ResourceGroupName() + + // Check if there is an ongoing long running operation. + future := scope.GetLongRunningOperationState(resourceName, serviceName) + if future != nil { + return ProcessOngoingOperation(ctx, scope, client, resourceName, serviceName) + } + + // No long running operation is active, so create the resource. + scope.V(2).Info("creating resource", "service", serviceName, "resource", resourceName, "resourceGroup", rgName) + sdkFuture, err := client.CreateOrUpdateAsync(ctx, spec) + if err != nil { + if sdkFuture != nil { + future, err := converters.SDKToFuture(sdkFuture, infrav1.PutFuture, serviceName, resourceName, rgName) + if err != nil { + return errors.Wrapf(err, "failed to create resource %s/%s (service: %s)", rgName, resourceName, serviceName) + } + scope.SetLongRunningOperationState(future) + return azure.WithTransientError(azure.NewOperationNotDoneError(future), reconciler.DefaultReconcilerRequeue) + } + + return errors.Wrapf(err, "failed to create resource %s/%s (service: %s)", rgName, resourceName, serviceName) + } + + scope.V(2).Info("successfully created resource", "service", serviceName, "resource", resourceName, "resourceGroup", rgName) + return nil +} + +// DeleteResource implements the logic for deleting a resource Asynchronously. +func DeleteResource(ctx context.Context, scope FutureScope, client Deleter, spec azure.ResourceSpecGetter, serviceName string) error { + resourceName := spec.ResourceName() + rgName := spec.ResourceGroupName() + + // Check if there is an ongoing long running operation. + future := scope.GetLongRunningOperationState(resourceName, serviceName) + if future != nil { + return ProcessOngoingOperation(ctx, scope, client, resourceName, serviceName) + } + + // No long running operation is active, so delete the resource. + scope.V(2).Info("deleting resource", "service", serviceName, "resource", resourceName, "resourceGroup", rgName) + sdkFuture, err := client.DeleteAsync(ctx, spec) + if err != nil { + if azure.ResourceNotFound(err) { + // already deleted + return nil + } else if sdkFuture != nil { + future, err := converters.SDKToFuture(sdkFuture, infrav1.DeleteFuture, serviceName, resourceName, rgName) + if err != nil { + return errors.Wrapf(err, "failed to delete resource %s/%s (service: %s)", rgName, resourceName, serviceName) + } + scope.SetLongRunningOperationState(future) + return azure.WithTransientError(azure.NewOperationNotDoneError(future), reconciler.DefaultReconcilerRequeue) + } + + return errors.Wrapf(err, "failed to delete resource %s/%s (service: %s)", rgName, resourceName, serviceName) + } + + scope.V(2).Info("successfully deleted resource", "service", serviceName, "resource", resourceName, "resourceGroup", rgName) + return nil +} diff --git a/azure/services/async/helper_test.go b/azure/services/async/helper_test.go new file mode 100644 index 000000000000..b96f1ab83e88 --- /dev/null +++ b/azure/services/async/helper_test.go @@ -0,0 +1,319 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package async + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/Azure/go-autorest/autorest" + azureautorest "github.com/Azure/go-autorest/autorest/azure" + "github.com/golang/mock/gomock" + . "github.com/onsi/gomega" + "k8s.io/klog/v2/klogr" + + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" + "sigs.k8s.io/cluster-api-provider-azure/azure/mock_azure" + "sigs.k8s.io/cluster-api-provider-azure/azure/services/async/mock_async" + gomockinternal "sigs.k8s.io/cluster-api-provider-azure/internal/test/matchers/gomock" +) + +var ( + validCreateFuture = infrav1.Future{ + Type: infrav1.PutFuture, + ServiceName: "test-service", + Name: "test-resource", + ResourceGroup: "test-group", + Data: "eyJtZXRob2QiOiJQVVQiLCJwb2xsaW5nTWV0aG9kIjoiTG9jYXRpb24iLCJscm9TdGF0ZSI6IkluUHJvZ3Jlc3MifQ==", + } + validDeleteFuture = infrav1.Future{ + Type: infrav1.DeleteFuture, + ServiceName: "test-service", + Name: "test-resource", + ResourceGroup: "test-group", + Data: "eyJtZXRob2QiOiJERUxFVEUiLCJwb2xsaW5nTWV0aG9kIjoiTG9jYXRpb24iLCJscm9TdGF0ZSI6IkluUHJvZ3Jlc3MifQ==", + } + invalidFuture = infrav1.Future{ + Type: infrav1.DeleteFuture, + ServiceName: "test-service", + Name: "test-resource", + ResourceGroup: "test-group", + Data: "ZmFrZSBiNjQgZnV0dXJlIGRhdGEK", + } + fakeError = autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error") + errCtxExceeded = errors.New("ctx exceeded") +) + +// TestProcessOngoingOperation tests the ProcessOngoingOperation function. +func TestProcessOngoingOperation(t *testing.T) { + testcases := []struct { + name string + resourceName string + serviceName string + expectedError string + expect func(s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockFutureHandlerMockRecorder) + }{ + { + name: "no future data stored in status", + expectedError: "", + resourceName: "test-resource", + serviceName: "test-service", + expect: func(s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockFutureHandlerMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + s.GetLongRunningOperationState("test-resource", "test-service").Return(nil) + }, + }, + { + name: "future data is not valid", + expectedError: "could not decode future data, resetting long-running operation state", + resourceName: "test-resource", + serviceName: "test-service", + expect: func(s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockFutureHandlerMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + s.GetLongRunningOperationState("test-resource", "test-service").Return(&invalidFuture) + s.DeleteLongRunningOperationState("test-resource", "test-service") + }, + }, + { + name: "fail to check if ongoing operation is done", + expectedError: "failed checking if the operation was complete", + resourceName: "test-resource", + serviceName: "test-service", + expect: func(s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockFutureHandlerMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + s.GetLongRunningOperationState("test-resource", "test-service").Return(&validDeleteFuture) + c.IsDone(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(false, fakeError) + }, + }, + { + name: "ongoing operation is not done", + expectedError: "operation type DELETE on Azure resource test-group/test-resource is not done", + resourceName: "test-resource", + serviceName: "test-service", + expect: func(s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockFutureHandlerMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + s.GetLongRunningOperationState("test-resource", "test-service").Return(&validDeleteFuture) + c.IsDone(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(false, nil) + }, + }, + { + name: "operation is done", + expectedError: "", + resourceName: "test-resource", + serviceName: "test-service", + expect: func(s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockFutureHandlerMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + s.GetLongRunningOperationState("test-resource", "test-service").Return(&validDeleteFuture) + c.IsDone(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(true, nil) + s.DeleteLongRunningOperationState("test-resource", "test-service") + }, + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + t.Parallel() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + scopeMock := mock_async.NewMockFutureScope(mockCtrl) + clientMock := mock_async.NewMockFutureHandler(mockCtrl) + + tc.expect(scopeMock.EXPECT(), clientMock.EXPECT()) + + err := ProcessOngoingOperation(context.TODO(), scopeMock, clientMock, tc.resourceName, tc.serviceName) + if tc.expectedError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tc.expectedError)) + } else { + g.Expect(err).NotTo(HaveOccurred()) + } + }) + } +} + +// TestCreateResource tests the CreateResource function. +func TestCreateResource(t *testing.T) { + testcases := []struct { + name string + serviceName string + expectedError string + expect func(s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockCreatorMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) + }{ + { + name: "create operation is already in progress", + expectedError: "transient reconcile error occurred: operation type PUT on Azure resource test-group/test-resource is not done. Object will be requeued after 15s", + serviceName: "test-service", + expect: func(s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockCreatorMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service").Times(2).Return(&validCreateFuture) + c.IsDone(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(false, nil) + }, + }, + { + name: "create async returns success", + expectedError: "", + serviceName: "test-service", + expect: func(s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockCreatorMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service").Return(nil) + c.CreateOrUpdateAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{})).Return(nil, nil) + }, + }, + { + name: "error occurs while running async create", + expectedError: "failed to create resource test-group/test-resource (service: test-service)", + serviceName: "test-service", + expect: func(s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockCreatorMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service").Return(nil) + c.CreateOrUpdateAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{})).Return(nil, fakeError) + }, + }, + { + name: "create async exits before completing", + expectedError: "transient reconcile error occurred: operation type PUT on Azure resource test-group/test-resource is not done. Object will be requeued after 15s", + serviceName: "test-service", + expect: func(s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockCreatorMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service").Return(nil) + c.CreateOrUpdateAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{})).Return(&azureautorest.Future{}, errCtxExceeded) + s.SetLongRunningOperationState(gomock.AssignableToTypeOf(&infrav1.Future{})) + }, + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + t.Parallel() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + scopeMock := mock_async.NewMockFutureScope(mockCtrl) + clientMock := mock_async.NewMockCreator(mockCtrl) + specMock := mock_azure.NewMockResourceSpecGetter(mockCtrl) + + tc.expect(scopeMock.EXPECT(), clientMock.EXPECT(), specMock.EXPECT()) + + err := CreateResource(context.TODO(), scopeMock, clientMock, specMock, tc.serviceName) + if tc.expectedError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tc.expectedError)) + } else { + g.Expect(err).NotTo(HaveOccurred()) + } + }) + } +} + +// TestDeleteResource tests the DeleteResource function. +func TestDeleteResource(t *testing.T) { + testcases := []struct { + name string + serviceName string + expectedError string + expect func(s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockDeleterMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) + }{ + { + name: "delete operation is already in progress", + expectedError: "transient reconcile error occurred: operation type DELETE on Azure resource test-group/test-resource is not done. Object will be requeued after 15s", + serviceName: "test-service", + expect: func(s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockDeleterMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service").Times(2).Return(&validDeleteFuture) + c.IsDone(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(false, nil) + }, + }, + { + name: "delete async returns success", + expectedError: "", + serviceName: "test-service", + expect: func(s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockDeleterMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service").Return(nil) + c.DeleteAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{})).Return(nil, nil) + }, + }, + { + name: "error occurs while running async delete", + expectedError: "failed to delete resource test-group/test-resource (service: test-service)", + serviceName: "test-service", + expect: func(s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockDeleterMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service").Return(nil) + c.DeleteAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{})).Return(nil, fakeError) + }, + }, + { + name: "delete async exits before completing", + expectedError: "transient reconcile error occurred: operation type DELETE on Azure resource test-group/test-resource is not done. Object will be requeued after 15s", + serviceName: "test-service", + expect: func(s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockDeleterMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service").Return(nil) + c.DeleteAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{})).Return(&azureautorest.Future{}, errCtxExceeded) + s.SetLongRunningOperationState(gomock.AssignableToTypeOf(&infrav1.Future{})) + }, + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + t.Parallel() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + scopeMock := mock_async.NewMockFutureScope(mockCtrl) + clientMock := mock_async.NewMockDeleter(mockCtrl) + specMock := mock_azure.NewMockResourceSpecGetter(mockCtrl) + + tc.expect(scopeMock.EXPECT(), clientMock.EXPECT(), specMock.EXPECT()) + + err := DeleteResource(context.TODO(), scopeMock, clientMock, specMock, tc.serviceName) + if tc.expectedError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tc.expectedError)) + } else { + g.Expect(err).NotTo(HaveOccurred()) + } + }) + } +} diff --git a/azure/services/async/interfaces.go b/azure/services/async/interfaces.go new file mode 100644 index 000000000000..0a847aa45021 --- /dev/null +++ b/azure/services/async/interfaces.go @@ -0,0 +1,49 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package async + +import ( + "context" + + azureautorest "github.com/Azure/go-autorest/autorest/azure" + "github.com/go-logr/logr" + "sigs.k8s.io/cluster-api-provider-azure/azure" +) + +// FutureScope is a scope that can perform store futures and conditions in Status. +type FutureScope interface { + logr.Logger + azure.AsyncStatusUpdater +} + +// FutureHandler is a client that can check on the progress of a future. +type FutureHandler interface { + // IsDone returns true if the operation is complete. + IsDone(ctx context.Context, future azureautorest.FutureAPI) (bool, error) +} + +// Creator is a client that can create or update a resource asynchronously. +type Creator interface { + FutureHandler + CreateOrUpdateAsync(ctx context.Context, spec azure.ResourceSpecGetter) (azureautorest.FutureAPI, error) +} + +// Deleter is a client that can delete a resource asynchronously. +type Deleter interface { + FutureHandler + DeleteAsync(ctx context.Context, spec azure.ResourceSpecGetter) (azureautorest.FutureAPI, error) +} diff --git a/azure/services/async/mock_async/async_mock.go b/azure/services/async/mock_async/async_mock.go new file mode 100644 index 000000000000..c6f8957f968e --- /dev/null +++ b/azure/services/async/mock_async/async_mock.go @@ -0,0 +1,368 @@ +/* +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: ../interfaces.go + +// Package mock_async is a generated GoMock package. +package mock_async + +import ( + context "context" + reflect "reflect" + + azure "github.com/Azure/go-autorest/autorest/azure" + logr "github.com/go-logr/logr" + gomock "github.com/golang/mock/gomock" + v1alpha4 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" + azure0 "sigs.k8s.io/cluster-api-provider-azure/azure" + v1alpha40 "sigs.k8s.io/cluster-api/api/v1alpha4" +) + +// MockFutureScope is a mock of FutureScope interface. +type MockFutureScope struct { + ctrl *gomock.Controller + recorder *MockFutureScopeMockRecorder +} + +// MockFutureScopeMockRecorder is the mock recorder for MockFutureScope. +type MockFutureScopeMockRecorder struct { + mock *MockFutureScope +} + +// NewMockFutureScope creates a new mock instance. +func NewMockFutureScope(ctrl *gomock.Controller) *MockFutureScope { + mock := &MockFutureScope{ctrl: ctrl} + mock.recorder = &MockFutureScopeMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFutureScope) EXPECT() *MockFutureScopeMockRecorder { + return m.recorder +} + +// DeleteLongRunningOperationState mocks base method. +func (m *MockFutureScope) DeleteLongRunningOperationState(arg0, arg1 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "DeleteLongRunningOperationState", arg0, arg1) +} + +// DeleteLongRunningOperationState indicates an expected call of DeleteLongRunningOperationState. +func (mr *MockFutureScopeMockRecorder) DeleteLongRunningOperationState(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLongRunningOperationState", reflect.TypeOf((*MockFutureScope)(nil).DeleteLongRunningOperationState), arg0, arg1) +} + +// Enabled mocks base method. +func (m *MockFutureScope) Enabled() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Enabled") + ret0, _ := ret[0].(bool) + return ret0 +} + +// Enabled indicates an expected call of Enabled. +func (mr *MockFutureScopeMockRecorder) Enabled() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enabled", reflect.TypeOf((*MockFutureScope)(nil).Enabled)) +} + +// Error mocks base method. +func (m *MockFutureScope) Error(err error, msg string, keysAndValues ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{err, msg} + for _, a := range keysAndValues { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Error", varargs...) +} + +// Error indicates an expected call of Error. +func (mr *MockFutureScopeMockRecorder) Error(err, msg interface{}, keysAndValues ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{err, msg}, keysAndValues...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockFutureScope)(nil).Error), varargs...) +} + +// GetLongRunningOperationState mocks base method. +func (m *MockFutureScope) GetLongRunningOperationState(arg0, arg1 string) *v1alpha4.Future { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLongRunningOperationState", arg0, arg1) + ret0, _ := ret[0].(*v1alpha4.Future) + return ret0 +} + +// GetLongRunningOperationState indicates an expected call of GetLongRunningOperationState. +func (mr *MockFutureScopeMockRecorder) GetLongRunningOperationState(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLongRunningOperationState", reflect.TypeOf((*MockFutureScope)(nil).GetLongRunningOperationState), arg0, arg1) +} + +// Info mocks base method. +func (m *MockFutureScope) Info(msg string, keysAndValues ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keysAndValues { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Info", varargs...) +} + +// Info indicates an expected call of Info. +func (mr *MockFutureScopeMockRecorder) Info(msg interface{}, keysAndValues ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keysAndValues...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockFutureScope)(nil).Info), varargs...) +} + +// SetLongRunningOperationState mocks base method. +func (m *MockFutureScope) SetLongRunningOperationState(arg0 *v1alpha4.Future) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetLongRunningOperationState", arg0) +} + +// SetLongRunningOperationState indicates an expected call of SetLongRunningOperationState. +func (mr *MockFutureScopeMockRecorder) SetLongRunningOperationState(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLongRunningOperationState", reflect.TypeOf((*MockFutureScope)(nil).SetLongRunningOperationState), arg0) +} + +// UpdateDeleteStatus mocks base method. +func (m *MockFutureScope) UpdateDeleteStatus(arg0 v1alpha40.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 *MockFutureScopeMockRecorder) UpdateDeleteStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeleteStatus", reflect.TypeOf((*MockFutureScope)(nil).UpdateDeleteStatus), arg0, arg1, arg2) +} + +// UpdatePatchStatus mocks base method. +func (m *MockFutureScope) UpdatePatchStatus(arg0 v1alpha40.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 *MockFutureScopeMockRecorder) UpdatePatchStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePatchStatus", reflect.TypeOf((*MockFutureScope)(nil).UpdatePatchStatus), arg0, arg1, arg2) +} + +// UpdatePutStatus mocks base method. +func (m *MockFutureScope) UpdatePutStatus(arg0 v1alpha40.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 *MockFutureScopeMockRecorder) UpdatePutStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePutStatus", reflect.TypeOf((*MockFutureScope)(nil).UpdatePutStatus), arg0, arg1, arg2) +} + +// V mocks base method. +func (m *MockFutureScope) V(level int) logr.Logger { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "V", level) + ret0, _ := ret[0].(logr.Logger) + return ret0 +} + +// V indicates an expected call of V. +func (mr *MockFutureScopeMockRecorder) V(level interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "V", reflect.TypeOf((*MockFutureScope)(nil).V), level) +} + +// WithName mocks base method. +func (m *MockFutureScope) WithName(name string) logr.Logger { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithName", name) + ret0, _ := ret[0].(logr.Logger) + return ret0 +} + +// WithName indicates an expected call of WithName. +func (mr *MockFutureScopeMockRecorder) WithName(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithName", reflect.TypeOf((*MockFutureScope)(nil).WithName), name) +} + +// WithValues mocks base method. +func (m *MockFutureScope) WithValues(keysAndValues ...interface{}) logr.Logger { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range keysAndValues { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "WithValues", varargs...) + ret0, _ := ret[0].(logr.Logger) + return ret0 +} + +// WithValues indicates an expected call of WithValues. +func (mr *MockFutureScopeMockRecorder) WithValues(keysAndValues ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithValues", reflect.TypeOf((*MockFutureScope)(nil).WithValues), keysAndValues...) +} + +// MockFutureHandler is a mock of FutureHandler interface. +type MockFutureHandler struct { + ctrl *gomock.Controller + recorder *MockFutureHandlerMockRecorder +} + +// MockFutureHandlerMockRecorder is the mock recorder for MockFutureHandler. +type MockFutureHandlerMockRecorder struct { + mock *MockFutureHandler +} + +// NewMockFutureHandler creates a new mock instance. +func NewMockFutureHandler(ctrl *gomock.Controller) *MockFutureHandler { + mock := &MockFutureHandler{ctrl: ctrl} + mock.recorder = &MockFutureHandlerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFutureHandler) EXPECT() *MockFutureHandlerMockRecorder { + return m.recorder +} + +// IsDone mocks base method. +func (m *MockFutureHandler) IsDone(ctx context.Context, future azure.FutureAPI) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsDone", ctx, future) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsDone indicates an expected call of IsDone. +func (mr *MockFutureHandlerMockRecorder) IsDone(ctx, future interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDone", reflect.TypeOf((*MockFutureHandler)(nil).IsDone), ctx, future) +} + +// MockCreator is a mock of Creator interface. +type MockCreator struct { + ctrl *gomock.Controller + recorder *MockCreatorMockRecorder +} + +// MockCreatorMockRecorder is the mock recorder for MockCreator. +type MockCreatorMockRecorder struct { + mock *MockCreator +} + +// NewMockCreator creates a new mock instance. +func NewMockCreator(ctrl *gomock.Controller) *MockCreator { + mock := &MockCreator{ctrl: ctrl} + mock.recorder = &MockCreatorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCreator) EXPECT() *MockCreatorMockRecorder { + return m.recorder +} + +// CreateOrUpdateAsync mocks base method. +func (m *MockCreator) CreateOrUpdateAsync(ctx context.Context, spec azure0.ResourceSpecGetter) (azure.FutureAPI, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateOrUpdateAsync", ctx, spec) + ret0, _ := ret[0].(azure.FutureAPI) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateOrUpdateAsync indicates an expected call of CreateOrUpdateAsync. +func (mr *MockCreatorMockRecorder) CreateOrUpdateAsync(ctx, spec interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateAsync", reflect.TypeOf((*MockCreator)(nil).CreateOrUpdateAsync), ctx, spec) +} + +// IsDone mocks base method. +func (m *MockCreator) IsDone(ctx context.Context, future azure.FutureAPI) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsDone", ctx, future) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsDone indicates an expected call of IsDone. +func (mr *MockCreatorMockRecorder) IsDone(ctx, future interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDone", reflect.TypeOf((*MockCreator)(nil).IsDone), ctx, future) +} + +// MockDeleter is a mock of Deleter interface. +type MockDeleter struct { + ctrl *gomock.Controller + recorder *MockDeleterMockRecorder +} + +// MockDeleterMockRecorder is the mock recorder for MockDeleter. +type MockDeleterMockRecorder struct { + mock *MockDeleter +} + +// NewMockDeleter creates a new mock instance. +func NewMockDeleter(ctrl *gomock.Controller) *MockDeleter { + mock := &MockDeleter{ctrl: ctrl} + mock.recorder = &MockDeleterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDeleter) EXPECT() *MockDeleterMockRecorder { + return m.recorder +} + +// DeleteAsync mocks base method. +func (m *MockDeleter) DeleteAsync(ctx context.Context, spec azure0.ResourceSpecGetter) (azure.FutureAPI, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteAsync", ctx, spec) + ret0, _ := ret[0].(azure.FutureAPI) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteAsync indicates an expected call of DeleteAsync. +func (mr *MockDeleterMockRecorder) DeleteAsync(ctx, spec interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAsync", reflect.TypeOf((*MockDeleter)(nil).DeleteAsync), ctx, spec) +} + +// IsDone mocks base method. +func (m *MockDeleter) IsDone(ctx context.Context, future azure.FutureAPI) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsDone", ctx, future) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsDone indicates an expected call of IsDone. +func (mr *MockDeleterMockRecorder) IsDone(ctx, future interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDone", reflect.TypeOf((*MockDeleter)(nil).IsDone), ctx, future) +} diff --git a/azure/services/async/mock_async/doc.go b/azure/services/async/mock_async/doc.go new file mode 100644 index 000000000000..124cb54e0e2a --- /dev/null +++ b/azure/services/async/mock_async/doc.go @@ -0,0 +1,20 @@ +/* +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. +*/ + +// Run go generate to regenerate this mock. +//go:generate ../../../../hack/tools/bin/mockgen -destination async_mock.go -package mock_async -source ../interfaces.go FutureHandler +//go:generate /usr/bin/env bash -c "cat ../../../../hack/boilerplate/boilerplate.generatego.txt async_mock.go > _async_mock.go && mv _async_mock.go async_mock.go" +package mock_async //nolint diff --git a/azure/services/scalesets/client.go b/azure/services/scalesets/client.go index 1d525e85c9cc..c13d8fe58ce9 100644 --- a/azure/services/scalesets/client.go +++ b/azure/services/scalesets/client.go @@ -24,7 +24,6 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2021-04-01/compute" - "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-11-01/network" "github.com/Azure/go-autorest/autorest" azureautorest "github.com/Azure/go-autorest/autorest/azure" "github.com/Azure/go-autorest/autorest/to" @@ -32,6 +31,7 @@ import ( infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" "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" ) @@ -40,15 +40,11 @@ type Client interface { List(context.Context, string) ([]compute.VirtualMachineScaleSet, error) ListInstances(context.Context, string, string) ([]compute.VirtualMachineScaleSetVM, error) Get(context.Context, string, string) (compute.VirtualMachineScaleSet, error) - CreateOrUpdate(context.Context, string, string, compute.VirtualMachineScaleSet) error - CreateOrUpdateAsync(context.Context, string, string, compute.VirtualMachineScaleSet) (*infrav1.Future, error) - Update(context.Context, string, string, compute.VirtualMachineScaleSetUpdate) (compute.VirtualMachineScaleSet, error) - UpdateAsync(context.Context, string, string, compute.VirtualMachineScaleSetUpdate) (*infrav1.Future, error) + CreateOrUpdateAsync(context.Context, string, string, compute.VirtualMachineScaleSet) (*compute.VirtualMachineScaleSetsCreateOrUpdateFuture, error) + UpdateAsync(context.Context, string, string, compute.VirtualMachineScaleSetUpdate) (*compute.VirtualMachineScaleSetsUpdateFuture, error) GetResultIfDone(ctx context.Context, future *infrav1.Future) (compute.VirtualMachineScaleSet, error) UpdateInstances(context.Context, string, string, []string) error - Delete(context.Context, string, string) error - DeleteAsync(context.Context, string, string) (*infrav1.Future, error) - GetPublicIPAddress(context.Context, string, string) (network.PublicIPAddress, error) + DeleteAsync(context.Context, string, string) (*compute.VirtualMachineScaleSetsDeleteFuture, error) } type ( @@ -56,7 +52,6 @@ type ( AzureClient struct { scalesetvms compute.VirtualMachineScaleSetVMsClient scalesets compute.VirtualMachineScaleSetsClient - publicIPs network.PublicIPAddressesClient } genericScaleSetFuture interface { @@ -74,15 +69,6 @@ type ( } ) -const ( - // PatchFuture is a future that was derived from a PATCH request to VMSS. - PatchFuture string = "PATCH" - // PutFuture is a future that was derived from a PUT request to VMSS. - PutFuture string = "PUT" - // DeleteFuture is a future that was derived from a DELETE request to VMSS. - DeleteFuture string = "DELETE" -) - var _ Client = &AzureClient{} // NewClient creates a new VMSS client from subscription ID. @@ -90,7 +76,6 @@ func NewClient(auth azure.Authorizer) *AzureClient { return &AzureClient{ scalesetvms: newVirtualMachineScaleSetVMsClient(auth.SubscriptionID(), auth.BaseURI(), auth.Authorizer()), scalesets: newVirtualMachineScaleSetsClient(auth.SubscriptionID(), auth.BaseURI(), auth.Authorizer()), - publicIPs: newPublicIPsClient(auth.SubscriptionID(), auth.BaseURI(), auth.Authorizer()), } } @@ -105,21 +90,6 @@ func newVirtualMachineScaleSetVMsClient(subscriptionID string, baseURI string, a func newVirtualMachineScaleSetsClient(subscriptionID string, baseURI string, authorizer autorest.Authorizer) compute.VirtualMachineScaleSetsClient { c := compute.NewVirtualMachineScaleSetsClientWithBaseURI(baseURI, subscriptionID) azure.SetAutoRestClientDefaults(&c.Client, authorizer) - - // The default number of retries is 3. This means the client will attempt to retry operation results like resource - // conflicts (HTTP 409). For a reconciling controller, this is undesirable behavior since if the controller runs - // into an error reconciling, the controller would be better off to end with an error and try again later. - // - // Unfortunately, the naming of this field is a bit misleading. This is not actually "retry attempts", it actually - // is attempts. Setting this to a value of 0 will cause a panic in Go AutoRest. - c.RetryAttempts = 1 - return c -} - -// newPublicIPsClient creates a new publicIPs client from subscription ID. -func newPublicIPsClient(subscriptionID string, baseURI string, authorizer autorest.Authorizer) network.PublicIPAddressesClient { - c := network.NewPublicIPAddressesClientWithBaseURI(baseURI, subscriptionID) - azure.SetAutoRestClientDefaults(&c.Client, authorizer) return c } @@ -173,26 +143,9 @@ func (ac *AzureClient) Get(ctx context.Context, resourceGroupName, vmssName stri return ac.scalesets.Get(ctx, resourceGroupName, vmssName, "") } -// CreateOrUpdate the operation to create or update a virtual machine scale set. -func (ac *AzureClient) CreateOrUpdate(ctx context.Context, resourceGroupName, vmssName string, vmss compute.VirtualMachineScaleSet) error { - ctx, span := tele.Tracer().Start(ctx, "scalesets.AzureClient.CreateOrUpdate") - defer span.End() - - future, err := ac.scalesets.CreateOrUpdate(ctx, resourceGroupName, vmssName, vmss) - if err != nil { - return err - } - err = future.WaitForCompletionRef(ctx, ac.scalesets.Client) - if err != nil { - return err - } - _, err = future.Result(ac.scalesets) - return err -} - // CreateOrUpdateAsync the operation to create or update a virtual machine scale set without waiting for the operation // to complete. -func (ac *AzureClient) CreateOrUpdateAsync(ctx context.Context, resourceGroupName, vmssName string, vmss compute.VirtualMachineScaleSet) (*infrav1.Future, error) { +func (ac *AzureClient) CreateOrUpdateAsync(ctx context.Context, resourceGroupName, vmssName string, vmss compute.VirtualMachineScaleSet) (*compute.VirtualMachineScaleSetsCreateOrUpdateFuture, error) { ctx, span := tele.Tracer().Start(ctx, "scalesets.AzureClient.CreateOrUpdateAsync") defer span.End() @@ -201,41 +154,21 @@ func (ac *AzureClient) CreateOrUpdateAsync(ctx context.Context, resourceGroupNam return nil, err } - jsonData, err := future.MarshalJSON() - if err != nil { - return nil, errors.Wrap(err, "failed to marshal async future") - } - - return &infrav1.Future{ - Type: PutFuture, - ResourceGroup: resourceGroupName, - Name: vmssName, - FutureData: base64.URLEncoding.EncodeToString(jsonData), - }, nil -} - -// Update update a VM scale set. -// Parameters: resourceGroupName - the name of the resource group. VMScaleSetName - the name of the VM scale set to create or update. parameters - the scale set object. -func (ac *AzureClient) Update(ctx context.Context, resourceGroupName, vmssName string, parameters compute.VirtualMachineScaleSetUpdate) (compute.VirtualMachineScaleSet, error) { - ctx, span := tele.Tracer().Start(ctx, "scalesets.AzureClient.Update") - defer span.End() - - future, err := ac.scalesets.Update(ctx, resourceGroupName, vmssName, parameters) - if err != nil { - return compute.VirtualMachineScaleSet{}, errors.Wrapf(err, "failed updating vmss named %q", vmssName) - } + ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureCallTimeout) + defer cancel() err = future.WaitForCompletionRef(ctx, ac.scalesets.Client) if err != nil { - return compute.VirtualMachineScaleSet{}, errors.Wrapf(err, "failed waiting for completion of operation for vmss named %q", vmssName) + // if an error occurs, return the future. + // this means the long-running operation didn't finish in the specified timeout. + return &future, nil } - vmss, err := future.Result(ac.scalesets) - if err != nil { - return vmss, errors.Wrapf(err, "failed fetching the result of operation for vmss named %q", vmssName) - } + // todo: this returns the result VMSS, we should use it + _, err = future.Result(ac.scalesets) - return vmss, nil + // if the operation completed, return a nil future. + return nil, err } // UpdateAsync update a VM scale set without waiting for the result of the operation. UpdateAsync sends a PATCH @@ -245,7 +178,7 @@ func (ac *AzureClient) Update(ctx context.Context, resourceGroupName, vmssName s // Parameters: // resourceGroupName - the name of the resource group. // vmssName - the name of the VM scale set to create or update. parameters - the scale set object. -func (ac *AzureClient) UpdateAsync(ctx context.Context, resourceGroupName, vmssName string, parameters compute.VirtualMachineScaleSetUpdate) (*infrav1.Future, error) { +func (ac *AzureClient) UpdateAsync(ctx context.Context, resourceGroupName, vmssName string, parameters compute.VirtualMachineScaleSetUpdate) (*compute.VirtualMachineScaleSetsUpdateFuture, error) { ctx, span := tele.Tracer().Start(ctx, "scalesets.AzureClient.UpdateAsync") defer span.End() @@ -254,29 +187,32 @@ func (ac *AzureClient) UpdateAsync(ctx context.Context, resourceGroupName, vmssN return nil, errors.Wrapf(err, "failed updating vmss named %q", vmssName) } - jsonData, err := future.MarshalJSON() + ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureCallTimeout) + defer cancel() + + err = future.WaitForCompletionRef(ctx, ac.scalesets.Client) if err != nil { - return nil, errors.Wrap(err, "failed to marshal async future") + // if an error occurs, return the future. + // this means the long-running operation didn't finish in the specified timeout. + return &future, nil } + // todo: this returns the result VMSS, we should use it + _, err = future.Result(ac.scalesets) - return &infrav1.Future{ - Type: PatchFuture, - ResourceGroup: resourceGroupName, - Name: vmssName, - FutureData: base64.URLEncoding.EncodeToString(jsonData), - }, nil + // if the operation completed, return a nil future. + return nil, err } // GetResultIfDone fetches the result of a long-running operation future if it is done. func (ac *AzureClient) GetResultIfDone(ctx context.Context, future *infrav1.Future) (compute.VirtualMachineScaleSet, error) { var genericFuture genericScaleSetFuture - futureData, err := base64.URLEncoding.DecodeString(future.FutureData) + futureData, err := base64.URLEncoding.DecodeString(future.Data) if err != nil { return compute.VirtualMachineScaleSet{}, errors.Wrap(err, "failed to base64 decode future data") } switch future.Type { - case PatchFuture: + case infrav1.PatchFuture: var future compute.VirtualMachineScaleSetsUpdateFuture if err := json.Unmarshal(futureData, &future); err != nil { return compute.VirtualMachineScaleSet{}, errors.Wrap(err, "failed to unmarshal future data") @@ -286,7 +222,7 @@ func (ac *AzureClient) GetResultIfDone(ctx context.Context, future *infrav1.Futu FutureAPI: &future, result: future.Result, } - case PutFuture: + case infrav1.PutFuture: var future compute.VirtualMachineScaleSetsCreateOrUpdateFuture if err := json.Unmarshal(futureData, &future); err != nil { return compute.VirtualMachineScaleSet{}, errors.Wrap(err, "failed to unmarshal future data") @@ -296,7 +232,7 @@ func (ac *AzureClient) GetResultIfDone(ctx context.Context, future *infrav1.Futu FutureAPI: &future, result: future.Result, } - case DeleteFuture: + case infrav1.DeleteFuture: var future compute.VirtualMachineScaleSetsDeleteFuture if err := json.Unmarshal(futureData, &future); err != nil { return compute.VirtualMachineScaleSet{}, errors.Wrap(err, "failed to unmarshal future data") @@ -346,23 +282,6 @@ func (ac *AzureClient) UpdateInstances(ctx context.Context, resourceGroupName, v return err } -// Delete the operation to delete a virtual machine scale set. -func (ac *AzureClient) Delete(ctx context.Context, resourceGroupName, vmssName string) error { - ctx, span := tele.Tracer().Start(ctx, "scalesets.AzureClient.Delete") - defer span.End() - - future, err := ac.scalesets.Delete(ctx, resourceGroupName, vmssName, to.BoolPtr(false)) - if err != nil { - return err - } - err = future.WaitForCompletionRef(ctx, ac.scalesets.Client) - if err != nil { - return err - } - _, err = future.Result(ac.scalesets) - return err -} - // DeleteAsync is the operation to delete a virtual machine scale set 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. @@ -370,7 +289,7 @@ func (ac *AzureClient) Delete(ctx context.Context, resourceGroupName, vmssName s // Parameters: // resourceGroupName - the name of the resource group. // vmssName - the name of the VM scale set to create or update. parameters - the scale set object. -func (ac *AzureClient) DeleteAsync(ctx context.Context, resourceGroupName, vmssName string) (*infrav1.Future, error) { +func (ac *AzureClient) DeleteAsync(ctx context.Context, resourceGroupName, vmssName string) (*compute.VirtualMachineScaleSetsDeleteFuture, error) { ctx, span := tele.Tracer().Start(ctx, "scalesets.AzureClient.DeleteAsync") defer span.End() @@ -379,25 +298,19 @@ func (ac *AzureClient) DeleteAsync(ctx context.Context, resourceGroupName, vmssN return nil, errors.Wrapf(err, "failed deleting vmss named %q", vmssName) } - jsonData, err := future.MarshalJSON() + ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureCallTimeout) + defer cancel() + + err = future.WaitForCompletionRef(ctx, ac.scalesets.Client) if err != nil { - return nil, errors.Wrap(err, "failed to marshal async future") + // if an error occurs, return the future. + // this means the long-running operation didn't finish in the specified timeout. + return &future, nil } + _, err = future.Result(ac.scalesets) - return &infrav1.Future{ - Type: DeleteFuture, - ResourceGroup: resourceGroupName, - Name: vmssName, - FutureData: base64.URLEncoding.EncodeToString(jsonData), - }, nil -} - -// GetPublicIPAddress gets the public IP address for the given public IP name. -func (ac *AzureClient) GetPublicIPAddress(ctx context.Context, resourceGroupName, publicIPName string) (network.PublicIPAddress, error) { - ctx, span := tele.Tracer().Start(ctx, "scalesets.AzureClient.GetPublicIPAddress") - defer span.End() - - return ac.publicIPs.Get(ctx, resourceGroupName, publicIPName, "true") + // if the operation completed, return a nil future. + return nil, err } // Result wraps the delete result so that we can treat it generically. The only thing we care about is if the delete diff --git a/azure/services/scalesets/mock_scalesets/client_mock.go b/azure/services/scalesets/mock_scalesets/client_mock.go index bc14837bf8d1..98ccfa8106b3 100644 --- a/azure/services/scalesets/mock_scalesets/client_mock.go +++ b/azure/services/scalesets/mock_scalesets/client_mock.go @@ -25,7 +25,6 @@ import ( reflect "reflect" compute "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2021-04-01/compute" - network "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-11-01/network" autorest "github.com/Azure/go-autorest/autorest" gomock "github.com/golang/mock/gomock" v1alpha4 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" @@ -54,25 +53,11 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder { return m.recorder } -// CreateOrUpdate mocks base method. -func (m *MockClient) CreateOrUpdate(arg0 context.Context, arg1, arg2 string, arg3 compute.VirtualMachineScaleSet) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateOrUpdate", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(error) - return ret0 -} - -// CreateOrUpdate indicates an expected call of CreateOrUpdate. -func (mr *MockClientMockRecorder) CreateOrUpdate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdate", reflect.TypeOf((*MockClient)(nil).CreateOrUpdate), arg0, arg1, arg2, arg3) -} - // CreateOrUpdateAsync mocks base method. -func (m *MockClient) CreateOrUpdateAsync(arg0 context.Context, arg1, arg2 string, arg3 compute.VirtualMachineScaleSet) (*v1alpha4.Future, error) { +func (m *MockClient) CreateOrUpdateAsync(arg0 context.Context, arg1, arg2 string, arg3 compute.VirtualMachineScaleSet) (*compute.VirtualMachineScaleSetsCreateOrUpdateFuture, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateOrUpdateAsync", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(*v1alpha4.Future) + ret0, _ := ret[0].(*compute.VirtualMachineScaleSetsCreateOrUpdateFuture) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -83,25 +68,11 @@ func (mr *MockClientMockRecorder) CreateOrUpdateAsync(arg0, arg1, arg2, arg3 int return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateAsync", reflect.TypeOf((*MockClient)(nil).CreateOrUpdateAsync), arg0, arg1, arg2, arg3) } -// Delete mocks base method. -func (m *MockClient) Delete(arg0 context.Context, arg1, arg2 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// Delete indicates an expected call of Delete. -func (mr *MockClientMockRecorder) Delete(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockClient)(nil).Delete), arg0, arg1, arg2) -} - // DeleteAsync mocks base method. -func (m *MockClient) DeleteAsync(arg0 context.Context, arg1, arg2 string) (*v1alpha4.Future, error) { +func (m *MockClient) DeleteAsync(arg0 context.Context, arg1, arg2 string) (*compute.VirtualMachineScaleSetsDeleteFuture, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteAsync", arg0, arg1, arg2) - ret0, _ := ret[0].(*v1alpha4.Future) + ret0, _ := ret[0].(*compute.VirtualMachineScaleSetsDeleteFuture) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -127,21 +98,6 @@ func (mr *MockClientMockRecorder) Get(arg0, arg1, arg2 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClient)(nil).Get), arg0, arg1, arg2) } -// GetPublicIPAddress mocks base method. -func (m *MockClient) GetPublicIPAddress(arg0 context.Context, arg1, arg2 string) (network.PublicIPAddress, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPublicIPAddress", arg0, arg1, arg2) - ret0, _ := ret[0].(network.PublicIPAddress) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetPublicIPAddress indicates an expected call of GetPublicIPAddress. -func (mr *MockClientMockRecorder) GetPublicIPAddress(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicIPAddress", reflect.TypeOf((*MockClient)(nil).GetPublicIPAddress), arg0, arg1, arg2) -} - // GetResultIfDone mocks base method. func (m *MockClient) GetResultIfDone(ctx context.Context, future *v1alpha4.Future) (compute.VirtualMachineScaleSet, error) { m.ctrl.T.Helper() @@ -187,26 +143,11 @@ func (mr *MockClientMockRecorder) ListInstances(arg0, arg1, arg2 interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInstances", reflect.TypeOf((*MockClient)(nil).ListInstances), arg0, arg1, arg2) } -// Update mocks base method. -func (m *MockClient) Update(arg0 context.Context, arg1, arg2 string, arg3 compute.VirtualMachineScaleSetUpdate) (compute.VirtualMachineScaleSet, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(compute.VirtualMachineScaleSet) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Update indicates an expected call of Update. -func (mr *MockClientMockRecorder) Update(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockClient)(nil).Update), arg0, arg1, arg2, arg3) -} - // UpdateAsync mocks base method. -func (m *MockClient) UpdateAsync(arg0 context.Context, arg1, arg2 string, arg3 compute.VirtualMachineScaleSetUpdate) (*v1alpha4.Future, error) { +func (m *MockClient) UpdateAsync(arg0 context.Context, arg1, arg2 string, arg3 compute.VirtualMachineScaleSetUpdate) (*compute.VirtualMachineScaleSetsUpdateFuture, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateAsync", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(*v1alpha4.Future) + ret0, _ := ret[0].(*compute.VirtualMachineScaleSetsUpdateFuture) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/azure/services/scalesets/mock_scalesets/scalesets_mock.go b/azure/services/scalesets/mock_scalesets/scalesets_mock.go index 0e2af5edfbde..af3c8a90a94c 100644 --- a/azure/services/scalesets/mock_scalesets/scalesets_mock.go +++ b/azure/services/scalesets/mock_scalesets/scalesets_mock.go @@ -29,6 +29,7 @@ import ( gomock "github.com/golang/mock/gomock" v1alpha4 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" azure "sigs.k8s.io/cluster-api-provider-azure/azure" + v1alpha40 "sigs.k8s.io/cluster-api/api/v1alpha4" ) // MockScaleSetScope is a mock of ScaleSetScope interface. @@ -180,6 +181,18 @@ func (mr *MockScaleSetScopeMockRecorder) ClusterName() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterName", reflect.TypeOf((*MockScaleSetScope)(nil).ClusterName)) } +// DeleteLongRunningOperationState mocks base method. +func (m *MockScaleSetScope) DeleteLongRunningOperationState(arg0, arg1 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "DeleteLongRunningOperationState", arg0, arg1) +} + +// DeleteLongRunningOperationState indicates an expected call of DeleteLongRunningOperationState. +func (mr *MockScaleSetScopeMockRecorder) DeleteLongRunningOperationState(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLongRunningOperationState", reflect.TypeOf((*MockScaleSetScope)(nil).DeleteLongRunningOperationState), arg0, arg1) +} + // Enabled mocks base method. func (m *MockScaleSetScope) Enabled() bool { m.ctrl.T.Helper() @@ -212,32 +225,32 @@ func (mr *MockScaleSetScopeMockRecorder) Error(err, msg interface{}, keysAndValu } // GetBootstrapData mocks base method. -func (m *MockScaleSetScope) GetBootstrapData(ctx context.Context) (string, error) { +func (m *MockScaleSetScope) GetBootstrapData(arg0 context.Context) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBootstrapData", ctx) + ret := m.ctrl.Call(m, "GetBootstrapData", arg0) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // GetBootstrapData indicates an expected call of GetBootstrapData. -func (mr *MockScaleSetScopeMockRecorder) GetBootstrapData(ctx interface{}) *gomock.Call { +func (mr *MockScaleSetScopeMockRecorder) GetBootstrapData(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBootstrapData", reflect.TypeOf((*MockScaleSetScope)(nil).GetBootstrapData), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBootstrapData", reflect.TypeOf((*MockScaleSetScope)(nil).GetBootstrapData), arg0) } // GetLongRunningOperationState mocks base method. -func (m *MockScaleSetScope) GetLongRunningOperationState() *v1alpha4.Future { +func (m *MockScaleSetScope) GetLongRunningOperationState(arg0, arg1 string) *v1alpha4.Future { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLongRunningOperationState") + ret := m.ctrl.Call(m, "GetLongRunningOperationState", arg0, arg1) ret0, _ := ret[0].(*v1alpha4.Future) return ret0 } // GetLongRunningOperationState indicates an expected call of GetLongRunningOperationState. -func (mr *MockScaleSetScopeMockRecorder) GetLongRunningOperationState() *gomock.Call { +func (mr *MockScaleSetScopeMockRecorder) GetLongRunningOperationState(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLongRunningOperationState", reflect.TypeOf((*MockScaleSetScope)(nil).GetLongRunningOperationState)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLongRunningOperationState", reflect.TypeOf((*MockScaleSetScope)(nil).GetLongRunningOperationState), arg0, arg1) } // GetVMImage mocks base method. @@ -431,6 +444,42 @@ func (mr *MockScaleSetScopeMockRecorder) TenantID() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TenantID", reflect.TypeOf((*MockScaleSetScope)(nil).TenantID)) } +// UpdateDeleteStatus mocks base method. +func (m *MockScaleSetScope) UpdateDeleteStatus(arg0 v1alpha40.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 *MockScaleSetScopeMockRecorder) UpdateDeleteStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeleteStatus", reflect.TypeOf((*MockScaleSetScope)(nil).UpdateDeleteStatus), arg0, arg1, arg2) +} + +// UpdatePatchStatus mocks base method. +func (m *MockScaleSetScope) UpdatePatchStatus(arg0 v1alpha40.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 *MockScaleSetScopeMockRecorder) UpdatePatchStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePatchStatus", reflect.TypeOf((*MockScaleSetScope)(nil).UpdatePatchStatus), arg0, arg1, arg2) +} + +// UpdatePutStatus mocks base method. +func (m *MockScaleSetScope) UpdatePutStatus(arg0 v1alpha40.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 *MockScaleSetScopeMockRecorder) UpdatePutStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePutStatus", reflect.TypeOf((*MockScaleSetScope)(nil).UpdatePutStatus), arg0, arg1, arg2) +} + // V mocks base method. func (m *MockScaleSetScope) V(level int) logr.Logger { m.ctrl.T.Helper() diff --git a/azure/services/scalesets/scalesets.go b/azure/services/scalesets/scalesets.go index 8c65aeca110a..b02b72a24299 100644 --- a/azure/services/scalesets/scalesets.go +++ b/azure/services/scalesets/scalesets.go @@ -33,29 +33,24 @@ import ( infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" "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/scope" "sigs.k8s.io/cluster-api-provider-azure/azure/services/resourceskus" "sigs.k8s.io/cluster-api-provider-azure/util/tele" ) -const ( - // UltraSSDStorageAccountType identifies the Ultra disk storage account type. - UltraSSDStorageAccountType = "UltraSSD_LRS" -) - type ( // ScaleSetScope defines the scope interface for a scale sets service. ScaleSetScope interface { logr.Logger azure.ClusterDescriber - GetBootstrapData(ctx context.Context) (string, error) - GetLongRunningOperationState() *infrav1.Future + azure.AsyncStatusUpdater + GetBootstrapData(context.Context) (string, error) GetVMImage() (*infrav1.Image, error) SaveVMImageToStatus(*infrav1.Image) MaxSurge() (int, error) ScaleSetSpec() azure.ScaleSetSpec VMSSExtensionSpecs() []azure.VMSSExtensionSpec SetAnnotation(string, string) - SetLongRunningOperationState(*infrav1.Future) SetProviderID(string) SetVMSSState(*azure.VMSS) } @@ -93,7 +88,7 @@ func (s *Service) Reconcile(ctx context.Context) (retErr error) { // check if there is an ongoing long running operation var ( - future = s.Scope.GetLongRunningOperationState() + future = s.Scope.GetLongRunningOperationState(s.Scope.ScaleSetSpec().Name, scope.ScalesetsServiceName) fetchedVMSS *azure.VMSS ) @@ -147,8 +142,8 @@ func (s *Service) Reconcile(ctx context.Context) (retErr error) { } } - // if we get to hear, we have completed any long running VMSS operations (creates / updates) - s.Scope.SetLongRunningOperationState(nil) + // if we get to here, we have completed any long running VMSS operations (creates / updates) + s.Scope.DeleteLongRunningOperationState(s.Scope.ScaleSetSpec().Name, scope.ScalesetsServiceName) return nil } @@ -175,7 +170,7 @@ func (s *Service) Delete(ctx context.Context) error { }() // check if there is an ongoing long running operation - future := s.Scope.GetLongRunningOperationState() + future := s.Scope.GetLongRunningOperationState(vmssSpec.Name, scope.ScalesetsServiceName) if future != nil { // if the operation is not complete this will return an error _, err := s.GetResultIfDone(ctx, future) @@ -184,13 +179,13 @@ func (s *Service) Delete(ctx context.Context) error { } // ScaleSet has been deleted - s.Scope.SetLongRunningOperationState(nil) + s.Scope.DeleteLongRunningOperationState(vmssSpec.Name, scope.ScalesetsServiceName) return nil } // no long running delete operation is active, so delete the ScaleSet s.Scope.V(2).Info("deleting VMSS", "scale set", vmssSpec.Name) - future, err = s.Client.DeleteAsync(ctx, s.Scope.ResourceGroup(), vmssSpec.Name) + sdkFuture, err := s.Client.DeleteAsync(ctx, s.Scope.ResourceGroup(), vmssSpec.Name) if err != nil { if azure.ResourceNotFound(err) { // already deleted @@ -199,6 +194,11 @@ func (s *Service) Delete(ctx context.Context) error { return errors.Wrapf(err, "failed to delete VMSS %s in resource group %s", vmssSpec.Name, s.Scope.ResourceGroup()) } + future, err = converters.SDKToFuture(sdkFuture, infrav1.DeleteFuture, scope.ScalesetsServiceName, vmssSpec.Name, s.Scope.ResourceGroup()) + if err != nil { + return errors.Wrapf(err, "failed to delete VMSS %s in resource group %s", vmssSpec.Name, s.Scope.ResourceGroup()) + } + s.Scope.SetLongRunningOperationState(future) if future != nil { // if future exists, check state of the future @@ -208,7 +208,7 @@ func (s *Service) Delete(ctx context.Context) error { } // future is either nil, or the result of the future is complete - s.Scope.SetLongRunningOperationState(nil) + s.Scope.DeleteLongRunningOperationState(vmssSpec.Name, scope.ScalesetsServiceName) return nil } @@ -223,9 +223,14 @@ func (s *Service) createVMSS(ctx context.Context) (*infrav1.Future, error) { return nil, errors.Wrap(err, "failed building VMSS from spec") } - future, err := s.Client.CreateOrUpdateAsync(ctx, s.Scope.ResourceGroup(), spec.Name, vmss) + sdkFuture, err := s.Client.CreateOrUpdateAsync(ctx, s.Scope.ResourceGroup(), spec.Name, vmss) if err != nil { - return future, errors.Wrap(err, "cannot create VMSS") + return nil, errors.Wrap(err, "cannot create VMSS") + } + + future, err := converters.SDKToFuture(sdkFuture, infrav1.PutFuture, scope.ScalesetsServiceName, spec.Name, s.Scope.ResourceGroup()) + if err != nil { + return future, errors.Wrapf(err, "failed to delete VMSS %s in resource group %s", spec.Name, s.Scope.ResourceGroup()) } s.Scope.V(2).Info("starting to create VMSS", "scale set", spec.Name) @@ -270,12 +275,17 @@ func (s *Service) patchVMSSIfNeeded(ctx context.Context, infraVMSS *azure.VMSS) } s.Scope.V(4).Info("patching vmss", "scale set", spec.Name, "patch", patch) - future, err := s.UpdateAsync(ctx, s.Scope.ResourceGroup(), spec.Name, patch) + sdkFuture, err := s.UpdateAsync(ctx, s.Scope.ResourceGroup(), spec.Name, patch) if err != nil { if azure.ResourceConflict(err) { - return future, azure.WithTransientError(err, 30*time.Second) + return nil, azure.WithTransientError(err, 30*time.Second) } - return future, errors.Wrap(err, "failed updating VMSS") + return nil, errors.Wrap(err, "failed updating VMSS") + } + + future, err := converters.SDKToFuture(sdkFuture, infrav1.PatchFuture, scope.ScalesetsServiceName, spec.Name, s.Scope.ResourceGroup()) + if err != nil { + return nil, errors.Wrapf(err, "failed to delete VMSS %s in resource group %s", spec.Name, s.Scope.ResourceGroup()) } s.Scope.SetLongRunningOperationState(future) @@ -337,7 +347,7 @@ func (s *Service) validateSpec(ctx context.Context) error { } for _, zone := range zones { - if disks.ManagedDisk != nil && disks.ManagedDisk.StorageAccountType == UltraSSDStorageAccountType && !sku.HasLocationCapability(resourceskus.UltraSSDAvailable, location, zone) { + if disks.ManagedDisk != nil && disks.ManagedDisk.StorageAccountType == string(compute.StorageAccountTypesUltraSSDLRS) && !sku.HasLocationCapability(resourceskus.UltraSSDAvailable, location, zone) { return azure.WithTerminalError(fmt.Errorf("vm size %s does not support ultra disks in location %s. select a different vm size or disable ultra disks", spec.Size, location)) } } @@ -482,7 +492,7 @@ func (s *Service) buildVMSSFromSpec(ctx context.Context, vmssSpec azure.ScaleSet } for _, dataDisk := range vmssSpec.DataDisks { - if dataDisk.ManagedDisk != nil && dataDisk.ManagedDisk.StorageAccountType == UltraSSDStorageAccountType { + if dataDisk.ManagedDisk != nil && dataDisk.ManagedDisk.StorageAccountType == string(compute.StorageAccountTypesUltraSSDLRS) { vmss.VirtualMachineScaleSetProperties.AdditionalCapabilities = &compute.AdditionalCapabilities{ UltraSSDEnabled: to.BoolPtr(true), } diff --git a/azure/services/scalesets/scalesets_test.go b/azure/services/scalesets/scalesets_test.go index 032c68651e00..e8c4d9417d3c 100644 --- a/azure/services/scalesets/scalesets_test.go +++ b/azure/services/scalesets/scalesets_test.go @@ -225,13 +225,13 @@ func TestGetExistingVMSS(t *testing.T) { func TestReconcileVMSS(t *testing.T) { var ( putFuture = &infrav1.Future{ - Type: PutFuture, + Type: infrav1.PutFuture, ResourceGroup: defaultResourceGroup, Name: defaultVMSSName, } patchFuture = &infrav1.Future{ - Type: PatchFuture, + Type: infrav1.PatchFuture, ResourceGroup: defaultResourceGroup, Name: defaultVMSSName, } @@ -273,7 +273,7 @@ func TestReconcileVMSS(t *testing.T) { createdVMSS := newDefaultVMSS("VM_SIZE") instances := newDefaultInstances() _ = setupDefaultVMSSInProgressOperationDoneExpectations(s, m, createdVMSS, instances) - s.SetLongRunningOperationState(nil) + s.DeleteLongRunningOperationState(defaultSpec.Name, scope.ScalesetsServiceName) }, }, { @@ -285,7 +285,7 @@ func TestReconcileVMSS(t *testing.T) { createdVMSS := newDefaultWindowsVMSS() instances := newDefaultInstances() _ = setupDefaultVMSSInProgressOperationDoneExpectations(s, m, createdVMSS, instances) - s.SetLongRunningOperationState(nil) + s.DeleteLongRunningOperationState(defaultSpec.Name, scope.ScalesetsServiceName) }, }, { @@ -624,11 +624,11 @@ func TestDeleteVMSS(t *testing.T) { s.ResourceGroup().AnyTimes().Return("my-existing-rg") s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) future := &infrav1.Future{} - s.GetLongRunningOperationState().Return(future) + s.GetLongRunningOperationState("my-existing-vmss", scope.ScalesetsServiceName).Return(future) m.GetResultIfDone(gomockinternal.AContext(), future).Return(compute.VirtualMachineScaleSet{}, nil) m.Get(gomockinternal.AContext(), "my-existing-rg", "my-existing-vmss"). Return(compute.VirtualMachineScaleSet{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) - s.SetLongRunningOperationState(nil) + s.DeleteLongRunningOperationState("my-existing-vmss", scope.ScalesetsServiceName) }, }, { @@ -642,7 +642,7 @@ func TestDeleteVMSS(t *testing.T) { }).AnyTimes() s.ResourceGroup().AnyTimes().Return(resourceGroup) s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) - s.GetLongRunningOperationState().Return(nil) + s.GetLongRunningOperationState(name, scope.ScalesetsServiceName).Return(nil) m.DeleteAsync(gomockinternal.AContext(), resourceGroup, name). Return(nil, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) m.Get(gomockinternal.AContext(), resourceGroup, name). @@ -660,7 +660,7 @@ func TestDeleteVMSS(t *testing.T) { }).AnyTimes() s.ResourceGroup().AnyTimes().Return(resourceGroup) s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) - s.GetLongRunningOperationState().Return(nil) + s.GetLongRunningOperationState(name, scope.ScalesetsServiceName).Return(nil) m.DeleteAsync(gomockinternal.AContext(), resourceGroup, name). Return(nil, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) m.Get(gomockinternal.AContext(), resourceGroup, name). @@ -1203,12 +1203,12 @@ func setupDefaultVMSSInProgressOperationDoneExpectations(s *mock_scalesets.MockS createdVMSS.ProvisioningState = to.StringPtr(string(infrav1.Succeeded)) setupDefaultVMSSExpectations(s) future := &infrav1.Future{ - Type: PutFuture, + Type: infrav1.PutFuture, ResourceGroup: defaultResourceGroup, Name: defaultVMSSName, - FutureData: "", + Data: "", } - s.GetLongRunningOperationState().Return(future) + s.GetLongRunningOperationState(defaultVMSSName, scope.ScalesetsServiceName).Return(future) m.GetResultIfDone(gomockinternal.AContext(), future).Return(createdVMSS, nil).AnyTimes() m.ListInstances(gomockinternal.AContext(), defaultResourceGroup, defaultVMSSName).Return(instances, nil).AnyTimes() s.MaxSurge().Return(1, nil) @@ -1219,7 +1219,7 @@ func setupDefaultVMSSInProgressOperationDoneExpectations(s *mock_scalesets.MockS func setupDefaultVMSSStartCreatingExpectations(s *mock_scalesets.MockScaleSetScopeMockRecorder, m *mock_scalesets.MockClientMockRecorder) { setupDefaultVMSSExpectations(s) - s.GetLongRunningOperationState().Return(nil) + s.GetLongRunningOperationState(defaultVMSSName, scope.ScalesetsServiceName).Return(nil) m.Get(gomockinternal.AContext(), defaultResourceGroup, defaultVMSSName). Return(compute.VirtualMachineScaleSet{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) } @@ -1285,7 +1285,7 @@ func setupVMSSExpectationsWithoutVMImage(s *mock_scalesets.MockScaleSetScopeMock func setupDefaultVMSSUpdateExpectations(s *mock_scalesets.MockScaleSetScopeMockRecorder) { setupUpdateVMSSExpectations(s) s.SetProviderID(azure.ProviderIDPrefix + "vmss-id") - s.GetLongRunningOperationState().Return(nil) + s.GetLongRunningOperationState(defaultVMSSName, scope.ScalesetsServiceName).Return(nil) s.MaxSurge().Return(1, nil) s.SetVMSSState(gomock.Any()) } diff --git a/azure/services/scalesetvms/client.go b/azure/services/scalesetvms/client.go index 928207162be8..ad7b324c3999 100644 --- a/azure/services/scalesetvms/client.go +++ b/azure/services/scalesetvms/client.go @@ -55,11 +55,6 @@ type ( } ) -const ( - // DeleteFuture is a future that was derived from a DELETE request to VMSS. - DeleteFuture string = "DELETE" -) - var _ client = &azureClient{} // newClient creates a new VMSS client from subscription ID. @@ -92,13 +87,13 @@ func (ac *azureClient) GetResultIfDone(ctx context.Context, future *infrav1.Futu defer span.End() var genericFuture genericScaleSetVMFuture - futureData, err := base64.URLEncoding.DecodeString(future.FutureData) + futureData, err := base64.URLEncoding.DecodeString(future.Data) if err != nil { return compute.VirtualMachineScaleSetVM{}, errors.Wrapf(err, "failed to base64 decode future data") } switch future.Type { - case DeleteFuture: + case infrav1.DeleteFuture: var future compute.VirtualMachineScaleSetVMsDeleteFuture if err := json.Unmarshal(futureData, &future); err != nil { return compute.VirtualMachineScaleSetVM{}, errors.Wrap(err, "failed to unmarshal future data") @@ -151,10 +146,10 @@ func (ac *azureClient) DeleteAsync(ctx context.Context, resourceGroupName, vmssN } return &infrav1.Future{ - Type: DeleteFuture, + Type: infrav1.DeleteFuture, ResourceGroup: resourceGroupName, - Name: vmssName, - FutureData: base64.URLEncoding.EncodeToString(jsonData), + Name: instanceID, + Data: base64.URLEncoding.EncodeToString(jsonData), }, nil } diff --git a/azure/services/scalesetvms/mock_scalesetvms/scalesetvms_mock.go b/azure/services/scalesetvms/mock_scalesetvms/scalesetvms_mock.go index 593a95eccf70..207151214e78 100644 --- a/azure/services/scalesetvms/mock_scalesetvms/scalesetvms_mock.go +++ b/azure/services/scalesetvms/mock_scalesetvms/scalesetvms_mock.go @@ -28,6 +28,7 @@ import ( gomock "github.com/golang/mock/gomock" v1alpha4 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" azure "sigs.k8s.io/cluster-api-provider-azure/azure" + v1alpha40 "sigs.k8s.io/cluster-api/api/v1alpha4" ) // MockScaleSetVMScope is a mock of ScaleSetVMScope interface. @@ -179,6 +180,18 @@ func (mr *MockScaleSetVMScopeMockRecorder) ClusterName() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterName", reflect.TypeOf((*MockScaleSetVMScope)(nil).ClusterName)) } +// DeleteLongRunningOperationState mocks base method. +func (m *MockScaleSetVMScope) DeleteLongRunningOperationState(arg0, arg1 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "DeleteLongRunningOperationState", arg0, arg1) +} + +// DeleteLongRunningOperationState indicates an expected call of DeleteLongRunningOperationState. +func (mr *MockScaleSetVMScopeMockRecorder) DeleteLongRunningOperationState(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLongRunningOperationState", reflect.TypeOf((*MockScaleSetVMScope)(nil).DeleteLongRunningOperationState), arg0, arg1) +} + // Enabled mocks base method. func (m *MockScaleSetVMScope) Enabled() bool { m.ctrl.T.Helper() @@ -211,17 +224,17 @@ func (mr *MockScaleSetVMScopeMockRecorder) Error(err, msg interface{}, keysAndVa } // GetLongRunningOperationState mocks base method. -func (m *MockScaleSetVMScope) GetLongRunningOperationState() *v1alpha4.Future { +func (m *MockScaleSetVMScope) GetLongRunningOperationState(arg0, arg1 string) *v1alpha4.Future { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLongRunningOperationState") + ret := m.ctrl.Call(m, "GetLongRunningOperationState", arg0, arg1) ret0, _ := ret[0].(*v1alpha4.Future) return ret0 } // GetLongRunningOperationState indicates an expected call of GetLongRunningOperationState. -func (mr *MockScaleSetVMScopeMockRecorder) GetLongRunningOperationState() *gomock.Call { +func (mr *MockScaleSetVMScopeMockRecorder) GetLongRunningOperationState(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLongRunningOperationState", reflect.TypeOf((*MockScaleSetVMScope)(nil).GetLongRunningOperationState)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLongRunningOperationState", reflect.TypeOf((*MockScaleSetVMScope)(nil).GetLongRunningOperationState), arg0, arg1) } // HashKey mocks base method. @@ -312,15 +325,15 @@ func (mr *MockScaleSetVMScopeMockRecorder) ScaleSetName() *gomock.Call { } // SetLongRunningOperationState mocks base method. -func (m *MockScaleSetVMScope) SetLongRunningOperationState(future *v1alpha4.Future) { +func (m *MockScaleSetVMScope) SetLongRunningOperationState(arg0 *v1alpha4.Future) { m.ctrl.T.Helper() - m.ctrl.Call(m, "SetLongRunningOperationState", future) + m.ctrl.Call(m, "SetLongRunningOperationState", arg0) } // SetLongRunningOperationState indicates an expected call of SetLongRunningOperationState. -func (mr *MockScaleSetVMScopeMockRecorder) SetLongRunningOperationState(future interface{}) *gomock.Call { +func (mr *MockScaleSetVMScopeMockRecorder) SetLongRunningOperationState(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLongRunningOperationState", reflect.TypeOf((*MockScaleSetVMScope)(nil).SetLongRunningOperationState), future) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLongRunningOperationState", reflect.TypeOf((*MockScaleSetVMScope)(nil).SetLongRunningOperationState), arg0) } // SetVMSSVM mocks base method. @@ -363,6 +376,42 @@ func (mr *MockScaleSetVMScopeMockRecorder) TenantID() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TenantID", reflect.TypeOf((*MockScaleSetVMScope)(nil).TenantID)) } +// UpdateDeleteStatus mocks base method. +func (m *MockScaleSetVMScope) UpdateDeleteStatus(arg0 v1alpha40.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 *MockScaleSetVMScopeMockRecorder) UpdateDeleteStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeleteStatus", reflect.TypeOf((*MockScaleSetVMScope)(nil).UpdateDeleteStatus), arg0, arg1, arg2) +} + +// UpdatePatchStatus mocks base method. +func (m *MockScaleSetVMScope) UpdatePatchStatus(arg0 v1alpha40.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 *MockScaleSetVMScopeMockRecorder) UpdatePatchStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePatchStatus", reflect.TypeOf((*MockScaleSetVMScope)(nil).UpdatePatchStatus), arg0, arg1, arg2) +} + +// UpdatePutStatus mocks base method. +func (m *MockScaleSetVMScope) UpdatePutStatus(arg0 v1alpha40.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 *MockScaleSetVMScopeMockRecorder) UpdatePutStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePutStatus", reflect.TypeOf((*MockScaleSetVMScope)(nil).UpdatePutStatus), arg0, arg1, arg2) +} + // V mocks base method. func (m *MockScaleSetVMScope) V(level int) logr.Logger { m.ctrl.T.Helper() diff --git a/azure/services/scalesetvms/scalesetvms.go b/azure/services/scalesetvms/scalesetvms.go index bda99d8d9051..737eaa36388c 100644 --- a/azure/services/scalesetvms/scalesetvms.go +++ b/azure/services/scalesetvms/scalesetvms.go @@ -29,16 +29,17 @@ import ( "sigs.k8s.io/cluster-api-provider-azure/util/tele" ) +const serviceName = "scalesetvms" + type ( // ScaleSetVMScope defines the scope interface for a scale sets service. ScaleSetVMScope interface { logr.Logger azure.ClusterDescriber + azure.AsyncStatusUpdater InstanceID() string ScaleSetName() string SetVMSSVM(vmssvm *azure.VMSSVM) - GetLongRunningOperationState() *infrav1.Future - SetLongRunningOperationState(future *infrav1.Future) } // Service provides operations on Azure resources. @@ -101,9 +102,9 @@ func (s *Service) Delete(ctx context.Context) error { }() log.V(4).Info("entering delete") - future := s.Scope.GetLongRunningOperationState() + future := s.Scope.GetLongRunningOperationState(instanceID, serviceName) if future != nil { - if future.Type != DeleteFuture { + if future.Type != infrav1.DeleteFuture { return azure.WithTransientError(errors.New("attempting to delete, non-delete operation in progress"), 30*time.Second) } @@ -115,7 +116,7 @@ func (s *Service) Delete(ctx context.Context) error { // there was no error in fetching the result, the future has been completed log.V(4).Info("successfully deleted the instance") - s.Scope.SetLongRunningOperationState(nil) + s.Scope.DeleteLongRunningOperationState(instanceID, serviceName) return nil } @@ -137,6 +138,6 @@ func (s *Service) Delete(ctx context.Context) error { return errors.Wrap(err, "failed to get result of long running operation") } - s.Scope.SetLongRunningOperationState(nil) + s.Scope.DeleteLongRunningOperationState(instanceID, serviceName) return nil } diff --git a/azure/services/scalesetvms/scalesetvms_test.go b/azure/services/scalesetvms/scalesetvms_test.go index 40052d710d8d..4c8fcfc7b586 100644 --- a/azure/services/scalesetvms/scalesetvms_test.go +++ b/azure/services/scalesetvms/scalesetvms_test.go @@ -176,9 +176,9 @@ func TestService_Delete(t *testing.T) { s.ResourceGroup().Return("rg") s.InstanceID().Return("0") s.ScaleSetName().Return("scaleset") - s.GetLongRunningOperationState().Return(nil) + s.GetLongRunningOperationState("0", serviceName).Return(nil) future := &infrav1.Future{ - Type: DeleteFuture, + Type: infrav1.DeleteFuture, } m.DeleteAsync(gomock2.AContext(), "rg", "scaleset", "0").Return(future, nil) s.SetLongRunningOperationState(future) @@ -187,7 +187,7 @@ func TestService_Delete(t *testing.T) { }, CheckIsErr: true, Err: errors.Wrap(azure.WithTransientError(azure.NewOperationNotDoneError(&infrav1.Future{ - Type: DeleteFuture, + Type: infrav1.DeleteFuture, }), 15*time.Second), "failed to get result of long running operation"), }, { @@ -197,9 +197,9 @@ func TestService_Delete(t *testing.T) { s.InstanceID().Return("0") s.ScaleSetName().Return("scaleset") future := &infrav1.Future{ - Type: DeleteFuture, + Type: infrav1.DeleteFuture, } - s.GetLongRunningOperationState().Return(future) + s.GetLongRunningOperationState("0", serviceName).Return(future) m.GetResultIfDone(gomock2.AContext(), future).Return(compute.VirtualMachineScaleSetVM{}, nil) s.SetLongRunningOperationState(nil) m.Get(gomock2.AContext(), "rg", "scaleset", "0").Return(compute.VirtualMachineScaleSetVM{}, nil) @@ -211,7 +211,7 @@ func TestService_Delete(t *testing.T) { s.ResourceGroup().Return("rg") s.InstanceID().Return("0") s.ScaleSetName().Return("scaleset") - s.GetLongRunningOperationState().Return(nil) + s.GetLongRunningOperationState("0", serviceName).Return(nil) m.DeleteAsync(gomock2.AContext(), "rg", "scaleset", "0").Return(nil, autorest404) m.Get(gomock2.AContext(), "rg", "scaleset", "0").Return(compute.VirtualMachineScaleSetVM{}, nil) }, @@ -222,7 +222,7 @@ func TestService_Delete(t *testing.T) { s.ResourceGroup().Return("rg") s.InstanceID().Return("0") s.ScaleSetName().Return("scaleset") - s.GetLongRunningOperationState().Return(nil) + s.GetLongRunningOperationState("0", serviceName).Return(nil) m.DeleteAsync(gomock2.AContext(), "rg", "scaleset", "0").Return(nil, errors.New("boom")) m.Get(gomock2.AContext(), "rg", "scaleset", "0").Return(compute.VirtualMachineScaleSetVM{}, nil) }, @@ -235,9 +235,9 @@ func TestService_Delete(t *testing.T) { s.InstanceID().Return("0") s.ScaleSetName().Return("scaleset") future := &infrav1.Future{ - Type: DeleteFuture, + Type: infrav1.DeleteFuture, } - s.GetLongRunningOperationState().Return(future) + s.GetLongRunningOperationState("0", serviceName).Return(future) m.GetResultIfDone(gomock2.AContext(), future).Return(compute.VirtualMachineScaleSetVM{}, errors.New("boom")) m.Get(gomock2.AContext(), "rg", "scaleset", "0").Return(compute.VirtualMachineScaleSetVM{}, nil) }, diff --git a/azure/services/virtualmachines/virtualmachines.go b/azure/services/virtualmachines/virtualmachines.go index 43ae5dba1399..e23c2ce1499c 100644 --- a/azure/services/virtualmachines/virtualmachines.go +++ b/azure/services/virtualmachines/virtualmachines.go @@ -39,11 +39,6 @@ import ( "sigs.k8s.io/cluster-api-provider-azure/util/tele" ) -const ( - // UltraSSDStorageAccountType identifies the Ultra disk storage account type. - UltraSSDStorageAccountType = "UltraSSD_LRS" -) - // VMScope defines the scope interface for a virtual machines service. type VMScope interface { logr.Logger @@ -199,7 +194,7 @@ func (s *Service) Reconcile(ctx context.Context) error { } for _, dataDisk := range vmSpec.DataDisks { - if dataDisk.ManagedDisk != nil && dataDisk.ManagedDisk.StorageAccountType == UltraSSDStorageAccountType { + if dataDisk.ManagedDisk != nil && dataDisk.ManagedDisk.StorageAccountType == string(compute.StorageAccountTypesUltraSSDLRS) { virtualMachine.VirtualMachineProperties.AdditionalCapabilities = &compute.AdditionalCapabilities{ UltraSSDEnabled: to.BoolPtr(true), } @@ -436,7 +431,7 @@ func (s *Service) generateStorageProfile(ctx context.Context, vmSpec azure.VMSpe // check the support for ultra disks based on location and vm size location := s.Scope.Location() - if disk.ManagedDisk.StorageAccountType == UltraSSDStorageAccountType && !sku.HasLocationCapability(resourceskus.UltraSSDAvailable, location, vmSpec.Zone) { + if disk.ManagedDisk.StorageAccountType == string(compute.StorageAccountTypesUltraSSDLRS) && !sku.HasLocationCapability(resourceskus.UltraSSDAvailable, location, vmSpec.Zone) { return nil, azure.WithTerminalError(fmt.Errorf("vm size %s does not support ultra disks in location %s. select a different vm size or disable ultra disks", vmSpec.Size, location)) } } diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml index 8ff27b9ec129..e73bb1faa6b1 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml @@ -413,9 +413,16 @@ spec: jsonPath: .metadata.labels.cluster\.x-k8s\.io/cluster-name name: Cluster type: string - - jsonPath: .status.ready + - jsonPath: .status.conditions[?(@.type=='Ready')].status name: Ready - type: boolean + type: string + - jsonPath: .status.conditions[?(@.type=='Ready')].reason + name: Reason + type: string + - jsonPath: .status.conditions[?(@.type=='Ready')].message + name: Message + priority: 1 + type: string - jsonPath: .spec.resourceGroup name: Resource Group priority: 1 @@ -1187,6 +1194,42 @@ spec: This list will be used by Cluster API to try and spread the machines across the failure domains.' type: object + longRunningOperationStates: + description: LongRunningOperationStates saves the states for Azure + long-running operations so they can be continued on the next reconciliation + loop. + items: + description: Future contains the data needed for an Azure long-running + operation to continue across reconcile loops. + properties: + data: + description: Data is the base64 url encoded json Azure AutoRest + Future. + type: string + name: + description: Name is the name of the Azure resource. Together + with the service name, this forms the unique identifier for + the future. + type: string + resourceGroup: + description: ResourceGroup is the Azure resource group for the + resource. + type: string + serviceName: + description: ServiceName is the name of the Azure service. Together + with the name of the resource, this forms the unique identifier + for the future. + type: string + type: + description: Type describes the type of future, such as update, + create, delete, etc. + type: string + required: + - name + - serviceName + - type + type: object + type: array ready: description: Ready is true when the provider resource is ready. type: boolean diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepoolmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepoolmachines.yaml index 563346bcb397..4a36fab855d3 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepoolmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepoolmachines.yaml @@ -152,29 +152,42 @@ spec: model, it means the instance may not be running the version of Kubernetes the Machine Pool has specified and needs to be updated. type: boolean - longRunningOperationState: - description: LongRunningOperationState saves the state for an Azure - long running operations so it can be continued on the next reconciliation + longRunningOperationStates: + description: LongRunningOperationStates saves the state for Azure + long running operations so they can be continued on the next reconciliation loop. - properties: - futureData: - description: FutureData is the base64 url encoded json Azure AutoRest - Future - type: string - name: - description: Name is the name of the Azure resource - type: string - resourceGroup: - description: ResourceGroup is the Azure resource group for the - resource - type: string - type: - description: Type describes the type of future, update, create, - delete, etc - type: string - required: - - type - type: object + items: + description: Future contains the data needed for an Azure long-running + operation to continue across reconcile loops. + properties: + data: + description: Data is the base64 url encoded json Azure AutoRest + Future. + type: string + name: + description: Name is the name of the Azure resource. Together + with the service name, this forms the unique identifier for + the future. + type: string + resourceGroup: + description: ResourceGroup is the Azure resource group for the + resource. + type: string + serviceName: + description: ServiceName is the name of the Azure service. Together + with the name of the resource, this forms the unique identifier + for the future. + type: string + type: + description: Type describes the type of future, such as update, + create, delete, etc. + type: string + required: + - name + - serviceName + - type + type: object + type: array nodeRef: description: NodeRef will point to the corresponding Node if it exists. properties: diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepools.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepools.yaml index 485f7d28946f..c59a23c6a62f 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepools.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinepools.yaml @@ -1237,29 +1237,42 @@ spec: - latestModelApplied type: object type: array - longRunningOperationState: - description: LongRunningOperationState saves the state for an Azure - long-running operations so it can be continued on the next reconciliation + longRunningOperationStates: + description: LongRunningOperationStates saves the state for Azure + long-running operations so they can be continued on the next reconciliation loop. - properties: - futureData: - description: FutureData is the base64 url encoded json Azure AutoRest - Future - type: string - name: - description: Name is the name of the Azure resource - type: string - resourceGroup: - description: ResourceGroup is the Azure resource group for the - resource - type: string - type: - description: Type describes the type of future, update, create, - delete, etc - type: string - required: - - type - type: object + items: + description: Future contains the data needed for an Azure long-running + operation to continue across reconcile loops. + properties: + data: + description: Data is the base64 url encoded json Azure AutoRest + Future. + type: string + name: + description: Name is the name of the Azure resource. Together + with the service name, this forms the unique identifier for + the future. + type: string + resourceGroup: + description: ResourceGroup is the Azure resource group for the + resource. + type: string + serviceName: + description: ServiceName is the name of the Azure service. Together + with the name of the resource, this forms the unique identifier + for the future. + type: string + type: + description: Type describes the type of future, such as update, + create, delete, etc. + type: string + required: + - name + - serviceName + - type + type: object + type: array provisioningState: description: ProvisioningState is the provisioning state of the Azure virtual machine. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml index 644701f84f07..66c43b4ff315 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml @@ -940,6 +940,42 @@ spec: during the reconciliation of Machines can be added as events to the Machine object and/or logged in the controller's output." type: string + longRunningOperationStates: + description: LongRunningOperationStates saves the states for Azure + long-running operations so they can be continued on the next reconciliation + loop. + items: + description: Future contains the data needed for an Azure long-running + operation to continue across reconcile loops. + properties: + data: + description: Data is the base64 url encoded json Azure AutoRest + Future. + type: string + name: + description: Name is the name of the Azure resource. Together + with the service name, this forms the unique identifier for + the future. + type: string + resourceGroup: + description: ResourceGroup is the Azure resource group for the + resource. + type: string + serviceName: + description: ServiceName is the name of the Azure service. Together + with the name of the resource, this forms the unique identifier + for the future. + type: string + type: + description: Type describes the type of future, such as update, + create, delete, etc. + type: string + required: + - name + - serviceName + - type + type: object + type: array ready: description: Ready is true when the provider resource is ready. type: boolean diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremanagedcontrolplanes.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremanagedcontrolplanes.yaml index d3db0c8dbc64..aeeaeb1fb509 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremanagedcontrolplanes.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremanagedcontrolplanes.yaml @@ -367,6 +367,42 @@ spec: fully ready. In the AzureManagedControlPlane implementation, these are identical. type: boolean + longRunningOperationStates: + description: LongRunningOperationStates saves the states for Azure + long-running operations so they can be continued on the next reconciliation + loop. + items: + description: Future contains the data needed for an Azure long-running + operation to continue across reconcile loops. + properties: + data: + description: Data is the base64 url encoded json Azure AutoRest + Future. + type: string + name: + description: Name is the name of the Azure resource. Together + with the service name, this forms the unique identifier for + the future. + type: string + resourceGroup: + description: ResourceGroup is the Azure resource group for the + resource. + type: string + serviceName: + description: ServiceName is the name of the Azure service. Together + with the name of the resource, this forms the unique identifier + for the future. + type: string + type: + description: Type describes the type of future, such as update, + create, delete, etc. + type: string + required: + - name + - serviceName + - type + type: object + type: array ready: description: Ready is true when the provider resource is ready. type: boolean diff --git a/controllers/azurecluster_controller.go b/controllers/azurecluster_controller.go index 7614ff2890fb..0dbbc40f282e 100644 --- a/controllers/azurecluster_controller.go +++ b/controllers/azurecluster_controller.go @@ -159,7 +159,7 @@ func (r *AzureClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request return reconcile.Result{}, err } if !scope.IsClusterNamespaceAllowed(ctx, r.Client, identity.Spec.AllowedNamespaces, azureCluster.Namespace) { - conditions.MarkFalse(azureCluster, infrav1.NetworkInfrastructureReadyCondition, infrav1.NamespaceNotAllowedByIdentity, clusterv1.ConditionSeverityError, "") + conditions.MarkFalse(azureCluster, clusterv1.ReadyCondition, infrav1.NamespaceNotAllowedByIdentity, clusterv1.ConditionSeverityError, "") return reconcile.Result{}, errors.New("AzureClusterIdentity list of allowed namespaces doesn't include current cluster namespace") } } else { @@ -229,7 +229,6 @@ func (r *AzureClusterReconciler) reconcileNormal(ctx context.Context, clusterSco // No errors, so mark us ready so the Cluster API Cluster Controller can pull it azureCluster.Status.Ready = true - conditions.MarkTrue(azureCluster, infrav1.NetworkInfrastructureReadyCondition) return reconcile.Result{}, nil } @@ -241,7 +240,7 @@ func (r *AzureClusterReconciler) reconcileDelete(ctx context.Context, clusterSco clusterScope.Info("Reconciling AzureCluster delete") azureCluster := clusterScope.AzureCluster - conditions.MarkFalse(azureCluster, infrav1.NetworkInfrastructureReadyCondition, clusterv1.DeletedReason, clusterv1.ConditionSeverityInfo, "") + conditions.MarkFalse(azureCluster, clusterv1.ReadyCondition, clusterv1.DeletedReason, clusterv1.ConditionSeverityInfo, "") if err := clusterScope.PatchObject(ctx); err != nil { return reconcile.Result{}, err } @@ -254,7 +253,7 @@ func (r *AzureClusterReconciler) reconcileDelete(ctx context.Context, clusterSco if err := acr.Delete(ctx); err != nil { wrappedErr := errors.Wrapf(err, "error deleting AzureCluster %s/%s", azureCluster.Namespace, azureCluster.Name) r.Recorder.Eventf(azureCluster, corev1.EventTypeWarning, "ClusterReconcilerDeleteFailed", wrappedErr.Error()) - conditions.MarkFalse(azureCluster, infrav1.NetworkInfrastructureReadyCondition, clusterv1.DeletionFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) + conditions.MarkFalse(azureCluster, clusterv1.ReadyCondition, clusterv1.DeletionFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) return reconcile.Result{}, wrappedErr } diff --git a/controllers/azurecluster_reconciler_test.go b/controllers/azurecluster_reconciler_test.go index c1a1c5c878c7..7c9af2fd4835 100644 --- a/controllers/azurecluster_reconciler_test.go +++ b/controllers/azurecluster_reconciler_test.go @@ -27,13 +27,13 @@ import ( infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" "sigs.k8s.io/cluster-api-provider-azure/azure" - "sigs.k8s.io/cluster-api-provider-azure/azure/mocks" + "sigs.k8s.io/cluster-api-provider-azure/azure/mock_azure" "sigs.k8s.io/cluster-api-provider-azure/azure/scope" "sigs.k8s.io/cluster-api-provider-azure/azure/services/resourceskus" gomockinternal "sigs.k8s.io/cluster-api-provider-azure/internal/test/matchers/gomock" ) -type expect func(grp *mocks.MockReconcilerMockRecorder, vnet *mocks.MockReconcilerMockRecorder, sg *mocks.MockReconcilerMockRecorder, rt *mocks.MockReconcilerMockRecorder, sn *mocks.MockReconcilerMockRecorder, natg *mocks.MockReconcilerMockRecorder, pip *mocks.MockReconcilerMockRecorder, lb *mocks.MockReconcilerMockRecorder, dns *mocks.MockReconcilerMockRecorder, bastion *mocks.MockReconcilerMockRecorder) +type expect func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder) func TestAzureClusterReconcilerDelete(t *testing.T) { cases := map[string]struct { @@ -42,21 +42,21 @@ func TestAzureClusterReconcilerDelete(t *testing.T) { }{ "Resource Group is deleted successfully": { expectedError: "", - expect: func(grp *mocks.MockReconcilerMockRecorder, vnet *mocks.MockReconcilerMockRecorder, sg *mocks.MockReconcilerMockRecorder, rt *mocks.MockReconcilerMockRecorder, sn *mocks.MockReconcilerMockRecorder, natg *mocks.MockReconcilerMockRecorder, pip *mocks.MockReconcilerMockRecorder, lb *mocks.MockReconcilerMockRecorder, dns *mocks.MockReconcilerMockRecorder, bastion *mocks.MockReconcilerMockRecorder) { + expect: func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder) { gomock.InOrder( grp.Delete(gomockinternal.AContext()).Return(nil)) }, }, "Resource Group delete fails": { expectedError: "failed to delete resource group: internal error", - expect: func(grp *mocks.MockReconcilerMockRecorder, vnet *mocks.MockReconcilerMockRecorder, sg *mocks.MockReconcilerMockRecorder, rt *mocks.MockReconcilerMockRecorder, sn *mocks.MockReconcilerMockRecorder, natg *mocks.MockReconcilerMockRecorder, pip *mocks.MockReconcilerMockRecorder, lb *mocks.MockReconcilerMockRecorder, dns *mocks.MockReconcilerMockRecorder, bastion *mocks.MockReconcilerMockRecorder) { + expect: func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder) { gomock.InOrder( grp.Delete(gomockinternal.AContext()).Return(errors.New("internal error"))) }, }, "Resource Group not owned by cluster": { expectedError: "", - expect: func(grp *mocks.MockReconcilerMockRecorder, vnet *mocks.MockReconcilerMockRecorder, sg *mocks.MockReconcilerMockRecorder, rt *mocks.MockReconcilerMockRecorder, sn *mocks.MockReconcilerMockRecorder, natg *mocks.MockReconcilerMockRecorder, pip *mocks.MockReconcilerMockRecorder, lb *mocks.MockReconcilerMockRecorder, dns *mocks.MockReconcilerMockRecorder, bastion *mocks.MockReconcilerMockRecorder) { + expect: func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder) { gomock.InOrder( grp.Delete(gomockinternal.AContext()).Return(azure.ErrNotOwned), bastion.Delete(gomockinternal.AContext()), @@ -73,7 +73,7 @@ func TestAzureClusterReconcilerDelete(t *testing.T) { }, "Load Balancer delete fails": { expectedError: "failed to delete load balancer: some error happened", - expect: func(grp *mocks.MockReconcilerMockRecorder, vnet *mocks.MockReconcilerMockRecorder, sg *mocks.MockReconcilerMockRecorder, rt *mocks.MockReconcilerMockRecorder, sn *mocks.MockReconcilerMockRecorder, pip *mocks.MockReconcilerMockRecorder, natg *mocks.MockReconcilerMockRecorder, lb *mocks.MockReconcilerMockRecorder, dns *mocks.MockReconcilerMockRecorder, bastion *mocks.MockReconcilerMockRecorder) { + expect: func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder) { gomock.InOrder( grp.Delete(gomockinternal.AContext()).Return(azure.ErrNotOwned), bastion.Delete(gomockinternal.AContext()), @@ -84,7 +84,7 @@ func TestAzureClusterReconcilerDelete(t *testing.T) { }, "Route table delete fails": { expectedError: "failed to delete route table: some error happened", - expect: func(grp *mocks.MockReconcilerMockRecorder, vnet *mocks.MockReconcilerMockRecorder, sg *mocks.MockReconcilerMockRecorder, rt *mocks.MockReconcilerMockRecorder, sn *mocks.MockReconcilerMockRecorder, pip *mocks.MockReconcilerMockRecorder, natg *mocks.MockReconcilerMockRecorder, lb *mocks.MockReconcilerMockRecorder, dns *mocks.MockReconcilerMockRecorder, bastion *mocks.MockReconcilerMockRecorder) { + expect: func(grp *mock_azure.MockReconcilerMockRecorder, vnet *mock_azure.MockReconcilerMockRecorder, sg *mock_azure.MockReconcilerMockRecorder, rt *mock_azure.MockReconcilerMockRecorder, sn *mock_azure.MockReconcilerMockRecorder, pip *mock_azure.MockReconcilerMockRecorder, natg *mock_azure.MockReconcilerMockRecorder, lb *mock_azure.MockReconcilerMockRecorder, dns *mock_azure.MockReconcilerMockRecorder, bastion *mock_azure.MockReconcilerMockRecorder) { gomock.InOrder( grp.Delete(gomockinternal.AContext()).Return(azure.ErrNotOwned), bastion.Delete(gomockinternal.AContext()), @@ -107,16 +107,16 @@ func TestAzureClusterReconcilerDelete(t *testing.T) { t.Parallel() mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() - groupsMock := mocks.NewMockReconciler(mockCtrl) - vnetMock := mocks.NewMockReconciler(mockCtrl) - sgMock := mocks.NewMockReconciler(mockCtrl) - rtMock := mocks.NewMockReconciler(mockCtrl) - subnetsMock := mocks.NewMockReconciler(mockCtrl) - natGatewaysMock := mocks.NewMockReconciler(mockCtrl) - publicIPMock := mocks.NewMockReconciler(mockCtrl) - lbMock := mocks.NewMockReconciler(mockCtrl) - dnsMock := mocks.NewMockReconciler(mockCtrl) - bastionMock := mocks.NewMockReconciler(mockCtrl) + groupsMock := mock_azure.NewMockReconciler(mockCtrl) + vnetMock := mock_azure.NewMockReconciler(mockCtrl) + sgMock := mock_azure.NewMockReconciler(mockCtrl) + rtMock := mock_azure.NewMockReconciler(mockCtrl) + subnetsMock := mock_azure.NewMockReconciler(mockCtrl) + natGatewaysMock := mock_azure.NewMockReconciler(mockCtrl) + publicIPMock := mock_azure.NewMockReconciler(mockCtrl) + lbMock := mock_azure.NewMockReconciler(mockCtrl) + dnsMock := mock_azure.NewMockReconciler(mockCtrl) + bastionMock := mock_azure.NewMockReconciler(mockCtrl) tc.expect(groupsMock.EXPECT(), vnetMock.EXPECT(), sgMock.EXPECT(), rtMock.EXPECT(), subnetsMock.EXPECT(), natGatewaysMock.EXPECT(), publicIPMock.EXPECT(), lbMock.EXPECT(), dnsMock.EXPECT(), bastionMock.EXPECT()) diff --git a/exp/api/v1alpha3/azuremachinepool_conversion.go b/exp/api/v1alpha3/azuremachinepool_conversion.go index 46061553b745..1f8899b452e4 100644 --- a/exp/api/v1alpha3/azuremachinepool_conversion.go +++ b/exp/api/v1alpha3/azuremachinepool_conversion.go @@ -18,6 +18,7 @@ package v1alpha3 import ( convert "k8s.io/apimachinery/pkg/conversion" + infrav1alpha3 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha3" infrav1alpha4 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" expv1alpha4 "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1alpha4" utilconversion "sigs.k8s.io/cluster-api/util/conversion" @@ -76,6 +77,12 @@ func (src *AzureMachinePool) ConvertTo(dstRaw conversion.Hub) error { // nolint dst.Annotations = nil } + for i, r := range restored.Status.LongRunningOperationStates { + if r.Name == dst.Status.LongRunningOperationStates[i].Name { + dst.Status.LongRunningOperationStates[i].ServiceName = r.ServiceName + } + } + return nil } @@ -104,5 +111,24 @@ func Convert_v1alpha4_AzureMachinePoolSpec_To_v1alpha3_AzureMachinePoolSpec(in * } func Convert_v1alpha4_AzureMachinePoolStatus_To_v1alpha3_AzureMachinePoolStatus(in *expv1alpha4.AzureMachinePoolStatus, out *AzureMachinePoolStatus, s convert.Scope) error { + if len(in.LongRunningOperationStates) > 0 { + if out.LongRunningOperationState == nil { + out.LongRunningOperationState = &infrav1alpha3.Future{} + } + if err := infrav1alpha3.Convert_v1alpha4_Future_To_v1alpha3_Future(&in.LongRunningOperationStates[0], out.LongRunningOperationState, s); err != nil { + return err + } + } return autoConvert_v1alpha4_AzureMachinePoolStatus_To_v1alpha3_AzureMachinePoolStatus(in, out, s) } + +func Convert_v1alpha3_AzureMachinePoolStatus_To_v1alpha4_AzureMachinePoolStatus(in *AzureMachinePoolStatus, out *expv1alpha4.AzureMachinePoolStatus, s convert.Scope) error { + if in.LongRunningOperationState != nil { + f := infrav1alpha4.Future{} + if err := infrav1alpha3.Convert_v1alpha3_Future_To_v1alpha4_Future(in.LongRunningOperationState, &f, s); err != nil { + return err + } + out.LongRunningOperationStates = []infrav1alpha4.Future{f} + } + return autoConvert_v1alpha3_AzureMachinePoolStatus_To_v1alpha4_AzureMachinePoolStatus(in, out, s) +} diff --git a/exp/api/v1alpha3/azuremanagedcontrolplane_conversion.go b/exp/api/v1alpha3/azuremanagedcontrolplane_conversion.go index 7e86dc15fb92..616f147587c3 100644 --- a/exp/api/v1alpha3/azuremanagedcontrolplane_conversion.go +++ b/exp/api/v1alpha3/azuremanagedcontrolplane_conversion.go @@ -39,6 +39,8 @@ func (src *AzureManagedControlPlane) ConvertTo(dstRaw conversion.Hub) error { // dst.Spec.IdentityRef = restored.Spec.IdentityRef + dst.Status.LongRunningOperationStates = restored.Status.LongRunningOperationStates + return nil } @@ -62,3 +64,8 @@ func (dst *AzureManagedControlPlane) ConvertFrom(srcRaw conversion.Hub) error { func Convert_v1alpha4_AzureManagedControlPlaneSpec_To_v1alpha3_AzureManagedControlPlaneSpec(in *expv1alpha4.AzureManagedControlPlaneSpec, out *AzureManagedControlPlaneSpec, s apiconversion.Scope) error { return autoConvert_v1alpha4_AzureManagedControlPlaneSpec_To_v1alpha3_AzureManagedControlPlaneSpec(in, out, s) } + +// Convert_v1alpha4_AzureManagedControlPlaneStatus_To_v1alpha3_AzureManagedControlPlaneStatus is an autogenerated conversion function. +func Convert_v1alpha4_AzureManagedControlPlaneStatus_To_v1alpha3_AzureManagedControlPlaneStatus(in *expv1alpha4.AzureManagedControlPlaneStatus, out *AzureManagedControlPlaneStatus, s apiconversion.Scope) error { + return autoConvert_v1alpha4_AzureManagedControlPlaneStatus_To_v1alpha3_AzureManagedControlPlaneStatus(in, out, s) +} diff --git a/exp/api/v1alpha3/zz_generated.conversion.go b/exp/api/v1alpha3/zz_generated.conversion.go index cb0d982489e5..881cf7ba233f 100644 --- a/exp/api/v1alpha3/zz_generated.conversion.go +++ b/exp/api/v1alpha3/zz_generated.conversion.go @@ -90,11 +90,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*AzureMachinePoolStatus)(nil), (*v1alpha4.AzureMachinePoolStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha3_AzureMachinePoolStatus_To_v1alpha4_AzureMachinePoolStatus(a.(*AzureMachinePoolStatus), b.(*v1alpha4.AzureMachinePoolStatus), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*AzureManagedCluster)(nil), (*v1alpha4.AzureManagedCluster)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha3_AzureManagedCluster_To_v1alpha4_AzureManagedCluster(a.(*AzureManagedCluster), b.(*v1alpha4.AzureManagedCluster), scope) }); err != nil { @@ -165,11 +160,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1alpha4.AzureManagedControlPlaneStatus)(nil), (*AzureManagedControlPlaneStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha4_AzureManagedControlPlaneStatus_To_v1alpha3_AzureManagedControlPlaneStatus(a.(*v1alpha4.AzureManagedControlPlaneStatus), b.(*AzureManagedControlPlaneStatus), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*AzureManagedMachinePool)(nil), (*v1alpha4.AzureManagedMachinePool)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha3_AzureManagedMachinePool_To_v1alpha4_AzureManagedMachinePool(a.(*AzureManagedMachinePool), b.(*v1alpha4.AzureManagedMachinePool), scope) }); err != nil { @@ -235,6 +225,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*AzureMachinePoolStatus)(nil), (*v1alpha4.AzureMachinePoolStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha3_AzureMachinePoolStatus_To_v1alpha4_AzureMachinePoolStatus(a.(*AzureMachinePoolStatus), b.(*v1alpha4.AzureMachinePoolStatus), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*clusterapiproviderazureapiv1alpha3.Image)(nil), (*clusterapiproviderazureapiv1alpha4.Image)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha3_Image_To_v1alpha4_Image(a.(*clusterapiproviderazureapiv1alpha3.Image), b.(*clusterapiproviderazureapiv1alpha4.Image), scope) }); err != nil { @@ -270,6 +265,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1alpha4.AzureManagedControlPlaneStatus)(nil), (*AzureManagedControlPlaneStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha4_AzureManagedControlPlaneStatus_To_v1alpha3_AzureManagedControlPlaneStatus(a.(*v1alpha4.AzureManagedControlPlaneStatus), b.(*AzureManagedControlPlaneStatus), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*clusterapiproviderazureapiv1alpha4.Image)(nil), (*clusterapiproviderazureapiv1alpha3.Image)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha4_Image_To_v1alpha3_Image(a.(*clusterapiproviderazureapiv1alpha4.Image), b.(*clusterapiproviderazureapiv1alpha3.Image), scope) }); err != nil { @@ -505,15 +505,10 @@ func autoConvert_v1alpha3_AzureMachinePoolStatus_To_v1alpha4_AzureMachinePoolSta out.FailureReason = (*errors.MachineStatusError)(unsafe.Pointer(in.FailureReason)) out.FailureMessage = (*string)(unsafe.Pointer(in.FailureMessage)) out.Conditions = *(*apiv1alpha4.Conditions)(unsafe.Pointer(&in.Conditions)) - out.LongRunningOperationState = (*clusterapiproviderazureapiv1alpha4.Future)(unsafe.Pointer(in.LongRunningOperationState)) + // WARNING: in.LongRunningOperationState requires manual conversion: does not exist in peer-type return nil } -// Convert_v1alpha3_AzureMachinePoolStatus_To_v1alpha4_AzureMachinePoolStatus is an autogenerated conversion function. -func Convert_v1alpha3_AzureMachinePoolStatus_To_v1alpha4_AzureMachinePoolStatus(in *AzureMachinePoolStatus, out *v1alpha4.AzureMachinePoolStatus, s conversion.Scope) error { - return autoConvert_v1alpha3_AzureMachinePoolStatus_To_v1alpha4_AzureMachinePoolStatus(in, out, s) -} - func autoConvert_v1alpha4_AzureMachinePoolStatus_To_v1alpha3_AzureMachinePoolStatus(in *v1alpha4.AzureMachinePoolStatus, out *AzureMachinePoolStatus, s conversion.Scope) error { out.Ready = in.Ready out.Replicas = in.Replicas @@ -524,7 +519,7 @@ func autoConvert_v1alpha4_AzureMachinePoolStatus_To_v1alpha3_AzureMachinePoolSta out.FailureReason = (*errors.MachineStatusError)(unsafe.Pointer(in.FailureReason)) out.FailureMessage = (*string)(unsafe.Pointer(in.FailureMessage)) out.Conditions = *(*apiv1alpha3.Conditions)(unsafe.Pointer(&in.Conditions)) - out.LongRunningOperationState = (*clusterapiproviderazureapiv1alpha3.Future)(unsafe.Pointer(in.LongRunningOperationState)) + // WARNING: in.LongRunningOperationStates requires manual conversion: does not exist in peer-type return nil } @@ -784,14 +779,10 @@ func Convert_v1alpha3_AzureManagedControlPlaneStatus_To_v1alpha4_AzureManagedCon func autoConvert_v1alpha4_AzureManagedControlPlaneStatus_To_v1alpha3_AzureManagedControlPlaneStatus(in *v1alpha4.AzureManagedControlPlaneStatus, out *AzureManagedControlPlaneStatus, s conversion.Scope) error { out.Ready = in.Ready out.Initialized = in.Initialized + // WARNING: in.LongRunningOperationStates requires manual conversion: does not exist in peer-type return nil } -// Convert_v1alpha4_AzureManagedControlPlaneStatus_To_v1alpha3_AzureManagedControlPlaneStatus is an autogenerated conversion function. -func Convert_v1alpha4_AzureManagedControlPlaneStatus_To_v1alpha3_AzureManagedControlPlaneStatus(in *v1alpha4.AzureManagedControlPlaneStatus, out *AzureManagedControlPlaneStatus, s conversion.Scope) error { - return autoConvert_v1alpha4_AzureManagedControlPlaneStatus_To_v1alpha3_AzureManagedControlPlaneStatus(in, out, s) -} - func autoConvert_v1alpha3_AzureManagedMachinePool_To_v1alpha4_AzureManagedMachinePool(in *AzureManagedMachinePool, out *v1alpha4.AzureManagedMachinePool, s conversion.Scope) error { out.ObjectMeta = in.ObjectMeta if err := Convert_v1alpha3_AzureManagedMachinePoolSpec_To_v1alpha4_AzureManagedMachinePoolSpec(&in.Spec, &out.Spec, s); err != nil { diff --git a/exp/api/v1alpha4/azuremachinepool_types.go b/exp/api/v1alpha4/azuremachinepool_types.go index 9ada3a2becfe..d5d8aa489221 100644 --- a/exp/api/v1alpha4/azuremachinepool_types.go +++ b/exp/api/v1alpha4/azuremachinepool_types.go @@ -282,10 +282,10 @@ type ( // +optional Conditions clusterv1.Conditions `json:"conditions,omitempty"` - // LongRunningOperationState saves the state for an Azure long-running operations so it can be continued on the + // LongRunningOperationStates saves the state for Azure long-running operations so they can be continued on the // next reconciliation loop. // +optional - LongRunningOperationState *infrav1.Future `json:"longRunningOperationState,omitempty"` + LongRunningOperationStates infrav1.Futures `json:"longRunningOperationStates,omitempty"` } // AzureMachinePoolInstanceStatus provides status information for each instance in the VMSS. @@ -357,6 +357,16 @@ func (amp *AzureMachinePool) SetConditions(conditions clusterv1.Conditions) { amp.Status.Conditions = conditions } +// GetFutures returns the list of long running operation states for an AzureMachinePool API object. +func (amp *AzureMachinePool) GetFutures() infrav1.Futures { + return amp.Status.LongRunningOperationStates +} + +// SetFutures will set the given long running operation states on an AzureMachinePool object. +func (amp *AzureMachinePool) SetFutures(futures infrav1.Futures) { + amp.Status.LongRunningOperationStates = futures +} + func init() { SchemeBuilder.Register(&AzureMachinePool{}, &AzureMachinePoolList{}) } diff --git a/exp/api/v1alpha4/azuremachinepoolmachine_types.go b/exp/api/v1alpha4/azuremachinepoolmachine_types.go index 870e540b5a12..ee42713a9c3f 100644 --- a/exp/api/v1alpha4/azuremachinepoolmachine_types.go +++ b/exp/api/v1alpha4/azuremachinepoolmachine_types.go @@ -83,10 +83,10 @@ type ( // +optional Conditions clusterv1.Conditions `json:"conditions,omitempty"` - // LongRunningOperationState saves the state for an Azure long running operations so it can be continued on the + // LongRunningOperationStates saves the state for Azure long running operations so they can be continued on the // next reconciliation loop. // +optional - LongRunningOperationState *infrav1.Future `json:"longRunningOperationState,omitempty"` + LongRunningOperationStates infrav1.Futures `json:"longRunningOperationStates,omitempty"` // LatestModelApplied indicates the instance is running the most up-to-date VMSS model. A VMSS model describes // the image version the VM is running. If the instance is not running the latest model, it means the instance @@ -136,6 +136,16 @@ func (ampm *AzureMachinePoolMachine) SetConditions(conditions clusterv1.Conditio ampm.Status.Conditions = conditions } +// GetFutures returns the list of long running operation states for an AzureCluster API object. +func (ampm *AzureMachinePoolMachine) GetFutures() infrav1.Futures { + return ampm.Status.LongRunningOperationStates +} + +// SetFutures will set the given long running operation states on an AzureCluster object. +func (ampm *AzureMachinePoolMachine) SetFutures(futures infrav1.Futures) { + ampm.Status.LongRunningOperationStates = futures +} + func init() { SchemeBuilder.Register(&AzureMachinePoolMachine{}, &AzureMachinePoolMachineList{}) } diff --git a/exp/api/v1alpha4/azuremanagedcontrolplane_types.go b/exp/api/v1alpha4/azuremanagedcontrolplane_types.go index 785ed8740652..05fafec880ef 100644 --- a/exp/api/v1alpha4/azuremanagedcontrolplane_types.go +++ b/exp/api/v1alpha4/azuremanagedcontrolplane_types.go @@ -124,6 +124,11 @@ type AzureManagedControlPlaneStatus struct { // In the AzureManagedControlPlane implementation, these are identical. // +optional Initialized bool `json:"initialized,omitempty"` + + // LongRunningOperationStates saves the states for Azure long-running operations so they can be continued on the + // next reconciliation loop. + // +optional + LongRunningOperationStates infrav1.Futures `json:"longRunningOperationStates,omitempty"` } // +kubebuilder:object:root=true @@ -149,6 +154,16 @@ type AzureManagedControlPlaneList struct { Items []AzureManagedControlPlane `json:"items"` } +// GetFutures returns the list of long running operation states for an AzureMachine API object. +func (m *AzureManagedControlPlane) GetFutures() infrav1.Futures { + return m.Status.LongRunningOperationStates +} + +// SetFutures will set the given long running operation states on an AzureMachine object. +func (m *AzureManagedControlPlane) SetFutures(futures infrav1.Futures) { + m.Status.LongRunningOperationStates = futures +} + func init() { SchemeBuilder.Register(&AzureManagedControlPlane{}, &AzureManagedControlPlaneList{}) } diff --git a/exp/api/v1alpha4/zz_generated.deepcopy.go b/exp/api/v1alpha4/zz_generated.deepcopy.go index a58fa5c93a3a..54dbabef6b6a 100644 --- a/exp/api/v1alpha4/zz_generated.deepcopy.go +++ b/exp/api/v1alpha4/zz_generated.deepcopy.go @@ -253,10 +253,10 @@ func (in *AzureMachinePoolMachineStatus) DeepCopyInto(out *AzureMachinePoolMachi (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.LongRunningOperationState != nil { - in, out := &in.LongRunningOperationState, &out.LongRunningOperationState - *out = new(apiv1alpha4.Future) - **out = **in + if in.LongRunningOperationStates != nil { + in, out := &in.LongRunningOperationStates, &out.LongRunningOperationStates + *out = make(apiv1alpha4.Futures, len(*in)) + copy(*out, *in) } } @@ -398,10 +398,10 @@ func (in *AzureMachinePoolStatus) DeepCopyInto(out *AzureMachinePoolStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.LongRunningOperationState != nil { - in, out := &in.LongRunningOperationState, &out.LongRunningOperationState - *out = new(apiv1alpha4.Future) - **out = **in + if in.LongRunningOperationStates != nil { + in, out := &in.LongRunningOperationStates, &out.LongRunningOperationStates + *out = make(apiv1alpha4.Futures, len(*in)) + copy(*out, *in) } } @@ -511,7 +511,7 @@ func (in *AzureManagedControlPlane) DeepCopyInto(out *AzureManagedControlPlane) out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureManagedControlPlane. @@ -621,6 +621,11 @@ func (in *AzureManagedControlPlaneSpec) DeepCopy() *AzureManagedControlPlaneSpec // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AzureManagedControlPlaneStatus) DeepCopyInto(out *AzureManagedControlPlaneStatus) { *out = *in + if in.LongRunningOperationStates != nil { + in, out := &in.LongRunningOperationStates, &out.LongRunningOperationStates + *out = make(apiv1alpha4.Futures, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureManagedControlPlaneStatus. diff --git a/exp/controllers/azuremachinepool_controller_unit_test.go b/exp/controllers/azuremachinepool_controller_unit_test.go index 75e2144f2491..081371b586a2 100644 --- a/exp/controllers/azuremachinepool_controller_unit_test.go +++ b/exp/controllers/azuremachinepool_controller_unit_test.go @@ -29,7 +29,7 @@ import ( clusterv1exp "sigs.k8s.io/cluster-api/exp/api/v1alpha4" infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" - "sigs.k8s.io/cluster-api-provider-azure/azure/mocks" + "sigs.k8s.io/cluster-api-provider-azure/azure/mock_azure" "sigs.k8s.io/cluster-api-provider-azure/azure/scope" infrav1exp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1alpha4" ) @@ -47,7 +47,7 @@ func Test_newAzureMachinePoolService(t *testing.T) { Vnet: infrav1.VnetSpec{Name: "my-vnet", ResourceGroup: "my-rg"}, } - clusterMock := mocks.NewMockClusterScoper(mockCtrl) + clusterMock := mock_azure.NewMockClusterScoper(mockCtrl) clusterMock.EXPECT().SubscriptionID().AnyTimes() clusterMock.EXPECT().BaseURI().AnyTimes() clusterMock.EXPECT().Authorizer().AnyTimes() diff --git a/exp/controllers/azuremachinepoolmachine_controller_test.go b/exp/controllers/azuremachinepoolmachine_controller_test.go index a2765384003a..39a1550c0efa 100644 --- a/exp/controllers/azuremachinepoolmachine_controller_test.go +++ b/exp/controllers/azuremachinepoolmachine_controller_test.go @@ -33,7 +33,7 @@ import ( "k8s.io/klog/v2/klogr" infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" "sigs.k8s.io/cluster-api-provider-azure/azure" - "sigs.k8s.io/cluster-api-provider-azure/azure/mocks" + "sigs.k8s.io/cluster-api-provider-azure/azure/mock_azure" "sigs.k8s.io/cluster-api-provider-azure/azure/scope" infrav1exp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1alpha4" gomock2 "sigs.k8s.io/cluster-api-provider-azure/internal/test/matchers/gomock" @@ -46,12 +46,12 @@ import ( func TestAzureMachinePoolMachineReconciler_Reconcile(t *testing.T) { cases := []struct { Name string - Setup func(cb *fake.ClientBuilder, reconciler *mocks.MockReconcilerMockRecorder) + Setup func(cb *fake.ClientBuilder, reconciler *mock_azure.MockReconcilerMockRecorder) Verify func(g *WithT, result ctrl.Result, err error) }{ { Name: "should successfully reconcile", - Setup: func(cb *fake.ClientBuilder, reconciler *mocks.MockReconcilerMockRecorder) { + Setup: func(cb *fake.ClientBuilder, reconciler *mock_azure.MockReconcilerMockRecorder) { cluster, azCluster, mp, amp, ampm := getAReadyMachinePoolMachineCluster() reconciler.Reconcile(gomock2.AContext()).Return(nil) cb.WithObjects(cluster, azCluster, mp, amp, ampm) @@ -62,7 +62,7 @@ func TestAzureMachinePoolMachineReconciler_Reconcile(t *testing.T) { }, { Name: "should successfully delete", - Setup: func(cb *fake.ClientBuilder, reconciler *mocks.MockReconcilerMockRecorder) { + Setup: func(cb *fake.ClientBuilder, reconciler *mock_azure.MockReconcilerMockRecorder) { cluster, azCluster, mp, amp, ampm := getAReadyMachinePoolMachineCluster() ampm.DeletionTimestamp = &metav1.Time{ Time: time.Now(), @@ -85,7 +85,7 @@ func TestAzureMachinePoolMachineReconciler_Reconcile(t *testing.T) { var ( g = NewWithT(t) mockCtrl = gomock.NewController(t) - reconciler = mocks.NewMockReconciler(mockCtrl) + reconciler = mock_azure.NewMockReconciler(mockCtrl) scheme = func() *runtime.Scheme { s := runtime.NewScheme() for _, addTo := range []func(s *runtime.Scheme) error{ diff --git a/test/e2e/aks.go b/test/e2e/aks.go index dee953ddf74f..dfaef4417291 100644 --- a/test/e2e/aks.go +++ b/test/e2e/aks.go @@ -22,6 +22,7 @@ import ( "context" "errors" "fmt" + "github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2020-02-01/containerservice" "github.com/Azure/go-autorest/autorest/azure/auth" . "github.com/onsi/ginkgo" diff --git a/test/e2e/azure_machinepool_drain.go b/test/e2e/azure_machinepool_drain.go index 0371eeaf6ea1..5a5ec3d4016b 100644 --- a/test/e2e/azure_machinepool_drain.go +++ b/test/e2e/azure_machinepool_drain.go @@ -93,14 +93,14 @@ func AzureMachinePoolDrainSpec(ctx context.Context, inputGetter func() AzureMach func testMachinePoolCordonAndDrain(ctx context.Context, mgmtClusterProxy, workloadClusterProxy framework.ClusterProxy, amp v1alpha4.AzureMachinePool) { var ( - isWindows = amp.Spec.Template.OSDisk.OSType == azure.WindowsOS - clientset = workloadClusterProxy.GetClientSet() - owningMachinePool = func() *clusterv1exp.MachinePool { + isWindows = amp.Spec.Template.OSDisk.OSType == azure.WindowsOS + clientset = workloadClusterProxy.GetClientSet() + owningMachinePool = func() *clusterv1exp.MachinePool { mp, err := getOwnerMachinePool(ctx, mgmtClusterProxy.GetClient(), amp.ObjectMeta) Expect(err).ToNot(HaveOccurred()) return mp }() - + machinePoolReplicas = func() int32 { Expect(owningMachinePool.Spec.Replicas).ToNot(BeNil(), "owning machine pool replicas must not be nil") Expect(*owningMachinePool.Spec.Replicas).To(BeNumerically(">=", 2), "owning machine pool replicas must be greater than or equal to 2") diff --git a/test/e2e/retry.go b/test/e2e/retry.go index 989d372df46d..7760c460121a 100644 --- a/test/e2e/retry.go +++ b/test/e2e/retry.go @@ -19,8 +19,9 @@ limitations under the License. package e2e import ( - "k8s.io/apimachinery/pkg/util/wait" "time" + + "k8s.io/apimachinery/pkg/util/wait" ) const ( diff --git a/util/futures/getter.go b/util/futures/getter.go new file mode 100644 index 000000000000..c8ab32258907 --- /dev/null +++ b/util/futures/getter.go @@ -0,0 +1,52 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package futures + +import ( + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Getter interface defines methods that an object should implement in order to +// use the futures package for getting long running operation states. +type Getter interface { + client.Object + + // GetFutures returns the list of long running operation states for an object. + GetFutures() infrav1.Futures +} + +// Get returns the future with the given name, if the future does not exists, +// it returns nil. +func Get(from Getter, name, service string) *infrav1.Future { + futures := from.GetFutures() + if futures == nil { + return nil + } + + for _, f := range futures { + if f.Name == name && f.ServiceName == service { + return &f + } + } + return nil +} + +// Has returns true if a future with the given name exists. +func Has(from Getter, name, service string) bool { + return Get(from, name, service) != nil +} diff --git a/util/futures/getter_test.go b/util/futures/getter_test.go new file mode 100644 index 000000000000..01b7419dff83 --- /dev/null +++ b/util/futures/getter_test.go @@ -0,0 +1,76 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package futures + +import ( + "testing" + + . "github.com/onsi/gomega" + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" +) + +func TestGet(t *testing.T) { + g := NewWithT(t) + + azurecluster := &infrav1.AzureCluster{} + + vmName := "my-vm" + vnetName := "my-vnet" + vm := "virtualmachines" + vnet := "virtualnetworks" + vmFuture := fakeFuture(vmName, vm) + vnetFuture := fakeFuture(vnetName, vnet) + + g.Expect(Get(azurecluster, vmName, vm)).To(BeNil()) + g.Expect(Get(azurecluster, vnetName, vnet)).To(BeNil()) + + azurecluster.SetFutures(infrav1.Futures{vmFuture, vnetFuture}) + + g.Expect(Get(azurecluster, vmName, vm)).To(Equal(&vmFuture)) + g.Expect(Get(azurecluster, vmName, vnet)).To(BeNil()) + g.Expect(Get(azurecluster, vnetName, vnet)).To(Equal(&vnetFuture)) +} + +func TestHas(t *testing.T) { + g := NewWithT(t) + + azurecluster := &infrav1.AzureCluster{} + + vmName := "my-vm" + vm := "virtualmachines" + vnet := "virtualnetworks" + vmFuture := fakeFuture(vmName, vm) + + g.Expect(Has(azurecluster, vmName, vm)).To(BeFalse()) + g.Expect(Has(azurecluster, "foo", vm)).To(BeFalse()) + + azurecluster.SetFutures(infrav1.Futures{vmFuture}) + + g.Expect(Has(azurecluster, vmName, vm)).To(BeTrue()) + g.Expect(Has(azurecluster, "foo", vm)).To(BeFalse()) + g.Expect(Has(azurecluster, vmName, vnet)).To(BeFalse()) +} + +func fakeFuture(name string, service string) infrav1.Future { + return infrav1.Future{ + Type: "PUT", + Name: name, + ResourceGroup: "test-rg", + Data: "", + ServiceName: service, + } +} diff --git a/util/futures/setter.go b/util/futures/setter.go new file mode 100644 index 000000000000..5ee0a84e4c38 --- /dev/null +++ b/util/futures/setter.go @@ -0,0 +1,70 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package futures + +import ( + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" +) + +// Setter interface defines methods that an object should implement in order to +// use the futures package for setting futures. +type Setter interface { + Getter + SetFutures(infrav1.Futures) +} + +// Set sets the given future. +// +// NOTE: If a future already exists, we update it. +func Set(to Setter, future *infrav1.Future) { + if to == nil || future == nil { + return + } + + // Check if the new future already exists, and update it if it does. + futures := to.GetFutures() + exists := false + for i, f := range futures { + if f.Name == future.Name && f.ServiceName == future.ServiceName { + exists = true + futures[i] = *future + } + } + + // If the future does not exist, add it. + if !exists { + futures = append(futures, *future) + } + + to.SetFutures(futures) +} + +// Delete deletes the specified future. +func Delete(to Setter, name, service string) { + if to == nil || name == "" || service == "" { + return + } + + futures := to.GetFutures() + for i := len(futures) - 1; i >= 0; i-- { + if futures[i].Name == name && futures[i].ServiceName == service { + futures = append(futures[:i], futures[i+1:]...) + } + } + + to.SetFutures(futures) +} diff --git a/util/futures/setter_test.go b/util/futures/setter_test.go new file mode 100644 index 000000000000..824772457d8e --- /dev/null +++ b/util/futures/setter_test.go @@ -0,0 +1,118 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package futures + +import ( + "testing" + + . "github.com/onsi/gomega" + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" +) + +func TestSet(t *testing.T) { + testService := "test-service" + a := fakeFuture("a", testService) + b := fakeFuture("b", testService) + newA := a + newA.Data = "new" + + tests := []struct { + name string + to Setter + future *infrav1.Future + want infrav1.Futures + }{ + { + name: "Set adds a future", + to: setterWithFutures(infrav1.Futures{}), + future: &a, + want: infrav1.Futures{a}, + }, + { + name: "Set adds more futures", + to: setterWithFutures(infrav1.Futures{a}), + future: &b, + want: infrav1.Futures{a, b}, + }, + { + name: "Set does not duplicate existing future", + to: setterWithFutures(infrav1.Futures{a, b}), + future: &a, + want: infrav1.Futures{a, b}, + }, + { + name: "Set updates an existing future", + to: setterWithFutures(infrav1.Futures{a, b}), + future: &newA, + want: infrav1.Futures{newA, b}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + Set(tt.to, tt.future) + + g.Expect(tt.to.GetFutures()).To(Equal(tt.want)) + }) + } +} + +func TestDelete(t *testing.T) { + testService := "test-service" + a := fakeFuture("a", testService) + b := fakeFuture("b", testService) + c := fakeFuture("c", testService) + d := fakeFuture("d", testService) + + tests := []struct { + name string + to Setter + future string + want infrav1.Futures + }{ + { + name: "Delete removes a future", + to: setterWithFutures(infrav1.Futures{a, b, c, d}), + future: "b", + want: infrav1.Futures{a, c, d}, + }, + { + name: "Delete does nothing if the future does not exist", + to: setterWithFutures(infrav1.Futures{a}), + future: "b", + want: infrav1.Futures{a}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + Delete(tt.to, tt.future, testService) + + g.Expect(tt.to.GetFutures()).To(Equal(tt.want)) + }) + } +} + +func setterWithFutures(futures infrav1.Futures) Setter { + obj := &infrav1.AzureCluster{} + obj.SetFutures(futures) + return obj +} diff --git a/util/reconciler/defaults.go b/util/reconciler/defaults.go index e6a8b6e59a55..5bcfc89acf3b 100644 --- a/util/reconciler/defaults.go +++ b/util/reconciler/defaults.go @@ -24,7 +24,13 @@ const ( // DefaultLoopTimeout is the default timeout for a reconcile loop (defaulted to the max ARM template duration). DefaultLoopTimeout = 90 * time.Minute // DefaultMappingTimeout is the default timeout for a controller request mapping func. - DefaultMappingTimeout = 60 * time.Second + DefaultMappingTimeout = 30 * time.Second // TODO: can this really take 30 seconds? + // DefaultAzureServiceReconcileTimeout is the default timeout for an Azure service reconcile. + DefaultAzureServiceReconcileTimeout = 12 * time.Second + // DefaultAzureCallTimeout is the default timeout for an Azure request after which an Azure operation is considered long running. + DefaultAzureCallTimeout = 2 * time.Second + // DefaultReconcilerRequeue is the default value for the reconcile retry. + DefaultReconcilerRequeue = 15 * time.Second ) // DefaultedLoopTimeout will default the timeout if it is zero-valued.