diff --git a/Makefile b/Makefile index 8c1d7200750..5337158d23f 100644 --- a/Makefile +++ b/Makefile @@ -90,7 +90,8 @@ test-unit: generate fmt vet manifests TEST_USE_EXISTING_CLUSTER=false REQUEUE_AFTER=20 \ go test -v -tags "$(BUILD_TAGS)" -coverprofile=reports/unittest-coverage-ouput.txt -covermode count -parallel 4 -timeout 10m \ ./pkg/resourcemanager/keyvaults/unittest/ \ - ./pkg/resourcemanager/azuresql/azuresqlfailovergroup + ./pkg/resourcemanager/azuresql/azuresqlfailovergroup \ + ./pkg/resourcemanager/cosmosdb/sqldatabase # The below folders are commented out because the tests in them fail... # ./api/... \ diff --git a/api/common.go b/api/common.go new file mode 100644 index 00000000000..f4ac5a1516b --- /dev/null +++ b/api/common.go @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package api + +// +kubebuilder:validation:Enum={"CreateOrUpdate","Delete"} +type PollingURLKind string + +const ( + PollingURLKindCreateOrUpdate = PollingURLKind("CreateOrUpdate") + PollingURLKindDelete = PollingURLKind("Delete") +) diff --git a/api/v1alpha1/aso_types.go b/api/v1alpha1/aso_types.go index 568f0784e0a..f701b75cd1a 100644 --- a/api/v1alpha1/aso_types.go +++ b/api/v1alpha1/aso_types.go @@ -3,26 +3,40 @@ package v1alpha1 -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + "github.com/Azure/azure-service-operator/api" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) // ASOStatus (AzureServiceOperatorsStatus) defines the observed state of resource actions type ASOStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file - Provisioning bool `json:"provisioning,omitempty"` - Provisioned bool `json:"provisioned,omitempty"` - State string `json:"state,omitempty"` - Message string `json:"message,omitempty"` - ResourceId string `json:"resourceId,omitempty"` - PollingURL string `json:"pollingUrl,omitempty"` - SpecHash string `json:"specHash,omitempty"` - ContainsUpdate bool `json:"containsUpdate,omitempty"` // TODO: Unused, remove in future version - RequestedAt *metav1.Time `json:"requested,omitempty"` - CompletedAt *metav1.Time `json:"completed,omitempty"` - FailedProvisioning bool `json:"failedProvisioning,omitempty"` - FlattenedSecrets bool `json:"flattenedSecrets,omitempty"` - Output string `json:"output,omitempty"` + Provisioning bool `json:"provisioning,omitempty"` + Provisioned bool `json:"provisioned,omitempty"` + State string `json:"state,omitempty"` + Message string `json:"message,omitempty"` + ResourceId string `json:"resourceId,omitempty"` + PollingURL string `json:"pollingUrl,omitempty"` + PollingURLKind *api.PollingURLKind `json:"pollingUrlKind,omitempty"` + SpecHash string `json:"specHash,omitempty"` + ContainsUpdate bool `json:"containsUpdate,omitempty"` // TODO: Unused, remove in future version + RequestedAt *metav1.Time `json:"requested,omitempty"` + CompletedAt *metav1.Time `json:"completed,omitempty"` + FailedProvisioning bool `json:"failedProvisioning,omitempty"` + FlattenedSecrets bool `json:"flattenedSecrets,omitempty"` + Output string `json:"output,omitempty"` +} + +func (s *ASOStatus) SetPollingURL(url string, kind api.PollingURLKind) { + s.PollingURL = url + s.PollingURLKind = &kind +} + +func (s *ASOStatus) ClearPollingURL() { + s.PollingURL = "" + s.PollingURLKind = nil } func (s *ASOStatus) SetProvisioned(msg string) { diff --git a/api/v1alpha1/cosmosdb_types.go b/api/v1alpha1/cosmosdb_types.go index 6aa0d23f8ee..5305bb66a63 100644 --- a/api/v1alpha1/cosmosdb_types.go +++ b/api/v1alpha1/cosmosdb_types.go @@ -16,8 +16,10 @@ type CosmosDBSpec struct { // Important: Run "make" to regenerate code after modifying this file // +kubebuilder:validation:MinLength=0 - + // +kubebuilder:validation:Required + // Location is the Azure location where the CosmosDB exists Location string `json:"location,omitempty"` + // +kubebuilder:validation:Pattern=^[-\w\._\(\)]+$ // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:Required diff --git a/api/v1alpha1/cosmosdbsqldatabase_types.go b/api/v1alpha1/cosmosdbsqldatabase_types.go new file mode 100644 index 00000000000..d9996468443 --- /dev/null +++ b/api/v1alpha1/cosmosdbsqldatabase_types.go @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// CosmosDBSQLDatabaseSpec defines the desired state of the CosmosDBSQLDatabase +type CosmosDBSQLDatabaseSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // +kubebuilder:validation:Pattern=^[-\w\._\(\)]+$ + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Required + // ResourceGroup is the resource group the CosmosDBSQLDatabase will be created in. + ResourceGroup string `json:"resourceGroup"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Required + // Account is the account that the SQL database will be created in. + Account string `json:"cosmosDBAccount"` + + // +kubebuilder:validation:Min=400 + // Throughput is the user specified manual throughput (RU/s) for the database expressed in units of 100 request + // units per second. The minimum is 400 up to 1,000,000 (or higher by requesting a limit increase). + // This must not be specified if autoscale is specified. This cannot be changed after creation if it + // (or autoscaleSettings) was not set to something initially. + Throughput *int32 `json:"throughput,omitempty"` + + // AutoscaleSettings contains the user specified autoscale configuration. + // This must not be specified if Throughput is specified. This cannot be changed after creation if it + // (or throughput) was not set to something initially. + AutoscaleSettings *AutoscaleSettings `json:"autoscaleSettings,omitempty"` + + // Tags are key-value pairs associated with the resource. + Tags map[string]string `json:"tags,omitempty"` +} + +type AutoscaleSettings struct { + // +kubebuilder:validation:Min=0 + // MaxThroughput is the autoscale max RU/s of the database. + MaxThroughput *int32 `json:"maxThroughput,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// CosmosDBSQLDatabase is the Schema for the cosmosdbsql API +// +kubebuilder:resource:shortName=cdbsql +// +kubebuilder:printcolumn:name="Provisioned",type="string",JSONPath=".status.provisioned" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.message" +type CosmosDBSQLDatabase struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec CosmosDBSQLDatabaseSpec `json:"spec,omitempty"` + Status ASOStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// CosmosDBSQLDatabaseList contains a list of CosmosDBSQLDatabase +type CosmosDBSQLDatabaseList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []CosmosDBSQLDatabase `json:"items"` +} + +func init() { + SchemeBuilder.Register(&CosmosDBSQLDatabase{}, &CosmosDBSQLDatabaseList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 72a2d66298b..826c7d956e2 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -11,6 +11,7 @@ package v1alpha1 import ( "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault" + "github.com/Azure/azure-service-operator/api" "k8s.io/apimachinery/pkg/runtime" ) @@ -128,6 +129,11 @@ func (in *APIVersionSet) DeepCopy() *APIVersionSet { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ASOStatus) DeepCopyInto(out *ASOStatus) { *out = *in + if in.PollingURLKind != nil { + in, out := &in.PollingURLKind, &out.PollingURLKind + *out = new(api.PollingURLKind) + **out = **in + } if in.RequestedAt != nil { in, out := &in.RequestedAt, &out.RequestedAt *out = (*in).DeepCopy() @@ -390,6 +396,26 @@ func (in *AppInsightsSpec) DeepCopy() *AppInsightsSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AutoscaleSettings) DeepCopyInto(out *AutoscaleSettings) { + *out = *in + if in.MaxThroughput != nil { + in, out := &in.MaxThroughput, &out.MaxThroughput + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoscaleSettings. +func (in *AutoscaleSettings) DeepCopy() *AutoscaleSettings { + if in == nil { + return nil + } + out := new(AutoscaleSettings) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AzureDBsSQLSku) DeepCopyInto(out *AzureDBsSQLSku) { *out = *in @@ -1752,6 +1778,97 @@ func (in *CosmosDBProperties) DeepCopy() *CosmosDBProperties { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CosmosDBSQLDatabase) DeepCopyInto(out *CosmosDBSQLDatabase) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CosmosDBSQLDatabase. +func (in *CosmosDBSQLDatabase) DeepCopy() *CosmosDBSQLDatabase { + if in == nil { + return nil + } + out := new(CosmosDBSQLDatabase) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CosmosDBSQLDatabase) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CosmosDBSQLDatabaseList) DeepCopyInto(out *CosmosDBSQLDatabaseList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CosmosDBSQLDatabase, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CosmosDBSQLDatabaseList. +func (in *CosmosDBSQLDatabaseList) DeepCopy() *CosmosDBSQLDatabaseList { + if in == nil { + return nil + } + out := new(CosmosDBSQLDatabaseList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CosmosDBSQLDatabaseList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CosmosDBSQLDatabaseSpec) DeepCopyInto(out *CosmosDBSQLDatabaseSpec) { + *out = *in + if in.Throughput != nil { + in, out := &in.Throughput, &out.Throughput + *out = new(int32) + **out = **in + } + if in.AutoscaleSettings != nil { + in, out := &in.AutoscaleSettings, &out.AutoscaleSettings + *out = new(AutoscaleSettings) + (*in).DeepCopyInto(*out) + } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CosmosDBSQLDatabaseSpec. +func (in *CosmosDBSQLDatabaseSpec) DeepCopy() *CosmosDBSQLDatabaseSpec { + if in == nil { + return nil + } + out := new(CosmosDBSQLDatabaseSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CosmosDBSpec) DeepCopyInto(out *CosmosDBSpec) { *out = *in diff --git a/api/v1alpha2/aso_types.go b/api/v1alpha2/aso_types.go index 0036841a487..e450dca0d1d 100644 --- a/api/v1alpha2/aso_types.go +++ b/api/v1alpha2/aso_types.go @@ -3,26 +3,40 @@ package v1alpha2 -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + "github.com/Azure/azure-service-operator/api" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) // ASOStatus (AzureServiceOperatorsStatus) defines the observed state of resource actions type ASOStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file - Provisioning bool `json:"provisioning,omitempty"` - Provisioned bool `json:"provisioned,omitempty"` - State string `json:"state,omitempty"` - Message string `json:"message,omitempty"` - ResourceId string `json:"resourceId,omitempty"` - PollingURL string `json:"pollingUrl,omitempty"` - SpecHash string `json:"specHash,omitempty"` - ContainsUpdate bool `json:"containsUpdate,omitempty"` // TODO: Unused, remove in future version - RequestedAt *metav1.Time `json:"requested,omitempty"` - CompletedAt *metav1.Time `json:"completed,omitempty"` - FailedProvisioning bool `json:"failedProvisioning,omitempty"` - FlattenedSecrets bool `json:"flattenedSecrets,omitempty"` - Output string `json:"output,omitempty"` + Provisioning bool `json:"provisioning,omitempty"` + Provisioned bool `json:"provisioned,omitempty"` + State string `json:"state,omitempty"` + Message string `json:"message,omitempty"` + ResourceId string `json:"resourceId,omitempty"` + PollingURL string `json:"pollingUrl,omitempty"` + PollingURLKind *api.PollingURLKind `json:"pollingUrlKind,omitempty"` + SpecHash string `json:"specHash,omitempty"` + ContainsUpdate bool `json:"containsUpdate,omitempty"` // TODO: Unused, remove in future version + RequestedAt *metav1.Time `json:"requested,omitempty"` + CompletedAt *metav1.Time `json:"completed,omitempty"` + FailedProvisioning bool `json:"failedProvisioning,omitempty"` + FlattenedSecrets bool `json:"flattenedSecrets,omitempty"` + Output string `json:"output,omitempty"` +} + +func (s *ASOStatus) SetPollingURL(url string, kind api.PollingURLKind) { + s.PollingURL = url + s.PollingURLKind = &kind +} + +func (s *ASOStatus) ClearPollingURL() { + s.PollingURL = "" + s.PollingURLKind = nil } func (s *ASOStatus) SetProvisioned(msg string) { diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go index cb8962d70f1..d8d6d74533b 100644 --- a/api/v1alpha2/zz_generated.deepcopy.go +++ b/api/v1alpha2/zz_generated.deepcopy.go @@ -10,12 +10,18 @@ Licensed under the MIT license. package v1alpha2 import ( + "github.com/Azure/azure-service-operator/api" runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ASOStatus) DeepCopyInto(out *ASOStatus) { *out = *in + if in.PollingURLKind != nil { + in, out := &in.PollingURLKind, &out.PollingURLKind + *out = new(api.PollingURLKind) + **out = **in + } if in.RequestedAt != nil { in, out := &in.RequestedAt, &out.RequestedAt *out = (*in).DeepCopy() diff --git a/api/v1beta1/aso_types.go b/api/v1beta1/aso_types.go index 84e2ae8331f..d13b3628a19 100644 --- a/api/v1beta1/aso_types.go +++ b/api/v1beta1/aso_types.go @@ -3,26 +3,40 @@ package v1beta1 -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + "github.com/Azure/azure-service-operator/api" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) // ASOStatus (AzureServiceOperatorsStatus) defines the observed state of resource actions type ASOStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file - Provisioning bool `json:"provisioning,omitempty"` - Provisioned bool `json:"provisioned,omitempty"` - State string `json:"state,omitempty"` - Message string `json:"message,omitempty"` - ResourceId string `json:"resourceId,omitempty"` - PollingURL string `json:"pollingUrl,omitempty"` - SpecHash string `json:"specHash,omitempty"` - ContainsUpdate bool `json:"containsUpdate,omitempty"` // TODO: Unused, remove in future version - RequestedAt *metav1.Time `json:"requested,omitempty"` - CompletedAt *metav1.Time `json:"completed,omitempty"` - FailedProvisioning bool `json:"failedProvisioning,omitempty"` - FlattenedSecrets bool `json:"flattenedSecrets,omitempty"` - Output string `json:"output,omitempty"` + Provisioning bool `json:"provisioning,omitempty"` + Provisioned bool `json:"provisioned,omitempty"` + State string `json:"state,omitempty"` + Message string `json:"message,omitempty"` + ResourceId string `json:"resourceId,omitempty"` + PollingURL string `json:"pollingUrl,omitempty"` + PollingURLKind *api.PollingURLKind `json:"pollingUrlKind,omitempty"` + SpecHash string `json:"specHash,omitempty"` + ContainsUpdate bool `json:"containsUpdate,omitempty"` // TODO: Unused, remove in future version + RequestedAt *metav1.Time `json:"requested,omitempty"` + CompletedAt *metav1.Time `json:"completed,omitempty"` + FailedProvisioning bool `json:"failedProvisioning,omitempty"` + FlattenedSecrets bool `json:"flattenedSecrets,omitempty"` + Output string `json:"output,omitempty"` +} + +func (s *ASOStatus) SetPollingURL(url string, kind api.PollingURLKind) { + s.PollingURL = url + s.PollingURLKind = &kind +} + +func (s *ASOStatus) ClearPollingURL() { + s.PollingURL = "" + s.PollingURLKind = nil } func (s *ASOStatus) SetProvisioned(msg string) { diff --git a/api/v1beta1/azuresqlfailovergroup_types.go b/api/v1beta1/azuresqlfailovergroup_types.go index 4d415fa156b..b101d5e0267 100644 --- a/api/v1beta1/azuresqlfailovergroup_types.go +++ b/api/v1beta1/azuresqlfailovergroup_types.go @@ -9,7 +9,6 @@ import ( // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - // ReadWriteEndpointFailoverPolicy - wraps https://godoc.org/github.com/Azure/azure-sdk-for-go/services/preview/sql/mgmt/v3.0/sql#ReadWriteEndpointFailoverPolicy // +kubebuilder:validation:Enum=Automatic;Manual type ReadWriteEndpointFailoverPolicy string diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 470ac54c9f0..165d14950bb 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -10,12 +10,18 @@ Licensed under the MIT license. package v1beta1 import ( + "github.com/Azure/azure-service-operator/api" runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ASOStatus) DeepCopyInto(out *ASOStatus) { *out = *in + if in.PollingURLKind != nil { + in, out := &in.PollingURLKind, &out.PollingURLKind + *out = new(api.PollingURLKind) + **out = **in + } if in.RequestedAt != nil { in, out := &in.RequestedAt, &out.RequestedAt *out = (*in).DeepCopy() diff --git a/config/samples/azure_v1alpha1_cosmosdbsqldatabase.yaml b/config/samples/azure_v1alpha1_cosmosdbsqldatabase.yaml new file mode 100644 index 00000000000..71c428819d7 --- /dev/null +++ b/config/samples/azure_v1alpha1_cosmosdbsqldatabase.yaml @@ -0,0 +1,16 @@ +apiVersion: azure.microsoft.com/v1alpha1 +kind: CosmosDBSQLDatabase +metadata: + name: cosmosdbsql-sample-1 +spec: + resourceGroup: resourcegroup-azure-operators + cosmosDBAccount: cosmosdb-sample-1 + + # Manual throughput (RU/s) for the database expressed in units of 100 request + # units per second. The minimum is 400 up to 1,000,000 (or higher by requesting a limit increase). + # This must not be specified if autoscaleSettings is specified. + #throughput: 500 + + #autoscaleSettings: + # the autoscale max RU/s of the database. This must not be specified if throughput is specified. + # maxThroughput: 1000 diff --git a/controllers/cosmosdb_controller_test.go b/controllers/cosmosdb_controller_test.go index 33957e469e6..117c353533a 100644 --- a/controllers/cosmosdb_controller_test.go +++ b/controllers/cosmosdb_controller_test.go @@ -50,6 +50,15 @@ func TestCosmosDBHappyPath(t *testing.T) { return err == nil && len(secret) > 0 }, tc.timeoutFast, tc.retry, "wait for cosmosdb to have secret") + t.Run("group1", func(t *testing.T) { + t.Run("Test Cosmos DB SQL Database happy path", func(t *testing.T) { + CosmosDBSQLDatabaseHappyPath(t, name) + }) + t.Run("Test Cosmos DB SQL Database failed throughput update", func(t *testing.T) { + CosmosDBSQLDatabase_FailedThroughputUpdate(t, name) + }) + }) + EnsureDelete(ctx, t, tc, dbInstance) assert.Eventually(func() bool { diff --git a/controllers/cosmosdbsqldatabase_controller.go b/controllers/cosmosdbsqldatabase_controller.go new file mode 100644 index 00000000000..57bba510ba3 --- /dev/null +++ b/controllers/cosmosdbsqldatabase_controller.go @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package controllers + +import ( + ctrl "sigs.k8s.io/controller-runtime" + + "github.com/Azure/azure-service-operator/api/v1alpha1" +) + +// CosmosDBSQLDatabaseReconciler reconciles a CosmosDB SQL Database object +type CosmosDBSQLDatabaseReconciler struct { + Reconciler *AsyncReconciler +} + +// +kubebuilder:rbac:groups=azure.microsoft.com,resources=cosmosdbsqldatabases,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=azure.microsoft.com,resources={cosmosdbsqldatabases/status,cosmosdbsqldatabases/finalizers},verbs=get;update;patch + +// Reconcile function does the main reconciliation loop of the operator +func (r *CosmosDBSQLDatabaseReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + return r.Reconciler.Reconcile(req, &v1alpha1.CosmosDBSQLDatabase{}) +} + +// SetupWithManager sets up the controller functions +func (r *CosmosDBSQLDatabaseReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.CosmosDBSQLDatabase{}). + Complete(r) +} diff --git a/controllers/cosmosdbsqldatabase_controller_test.go b/controllers/cosmosdbsqldatabase_controller_test.go new file mode 100644 index 00000000000..08388801397 --- /dev/null +++ b/controllers/cosmosdbsqldatabase_controller_test.go @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// +build all cosmos + +package controllers + +import ( + "context" + "strings" + "testing" + + "github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2021-03-15/documentdb" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/pkg/errhelp" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/cosmosdb" +) + +func CosmosDBSQLDatabaseHappyPath(t *testing.T, cosmosDBAccountName string) { + t.Parallel() + defer PanicRecover(t) + ctx := context.Background() + assert := require.New(t) + + cosmosDBSQLDatabaseClient, err := cosmosdb.GetCosmosDBSQLDatabaseClient(config.GlobalCredentials()) + assert.Equal(nil, err, "failed to get cosmos db SQL database client") + + name := GenerateTestResourceNameWithRandom("cosmosdbsql", 8) + namespace := "default" + + var throughPutRUs int32 = 400 + sqlDBInstance := &v1alpha1.CosmosDBSQLDatabase{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: v1alpha1.CosmosDBSQLDatabaseSpec{ + ResourceGroup: tc.resourceGroupName, + Account: cosmosDBAccountName, + Throughput: &throughPutRUs, + }, + } + + // Create the Cosmos SQL DB + EnsureInstance(ctx, t, tc, sqlDBInstance) + assert.Eventually(func() bool { + cosmosSQLDB, err := cosmosDBSQLDatabaseClient.GetSQLDatabase(ctx, tc.resourceGroupName, cosmosDBAccountName, name) + assert.Equal(nil, err, "err getting DB from Azure") + + throughputMatches, err := doesThroughputMatch( + ctx, + cosmosDBSQLDatabaseClient, + tc.resourceGroupName, + cosmosDBAccountName, + name, + throughPutRUs) + assert.Equal(nil, err, "err ensuring throughput matches") + + return cosmosSQLDB.Name != nil && *cosmosSQLDB.Name == name && throughputMatches + }, tc.timeout, tc.retry, "wait for cosmos SQL DB to exist in Azure") + + // Get the updated Cosmos SQL DB from k8s + namespacedName := types.NamespacedName{Name: name, Namespace: namespace} + err = tc.k8sClient.Get(ctx, namespacedName, sqlDBInstance) + assert.Equal(nil, err, "getting cosmos SQL DB from k8s") + + // Update the Cosmos SQL DB throughput + throughPutRUs = 1000 + sqlDBInstance.Spec.Throughput = &throughPutRUs + err = tc.k8sClient.Update(ctx, sqlDBInstance) + assert.Equal(nil, err, "updating cosmos sql DB in k8s") + + assert.Eventually(func() bool { + throughputMatches, err := doesThroughputMatch( + ctx, + cosmosDBSQLDatabaseClient, + tc.resourceGroupName, + cosmosDBAccountName, + name, + throughPutRUs) + assert.Equal(nil, err, "err ensuring throughput matches") + + return throughputMatches + }, tc.timeout, tc.retry, "wait for cosmos SQL DB throughput to be updated in Azure") + + EnsureDelete(ctx, t, tc, sqlDBInstance) +} + +func CosmosDBSQLDatabase_FailedThroughputUpdate(t *testing.T, cosmosDBAccountName string) { + t.Parallel() + defer PanicRecover(t) + ctx := context.Background() + assert := require.New(t) + + name := GenerateTestResourceNameWithRandom("cosmosdbsql", 8) + namespace := "default" + + sqlDBInstance := &v1alpha1.CosmosDBSQLDatabase{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: v1alpha1.CosmosDBSQLDatabaseSpec{ + ResourceGroup: tc.resourceGroupName, + Account: cosmosDBAccountName, + }, + } + + // Create the Cosmos SQL DB + EnsureInstance(ctx, t, tc, sqlDBInstance) + + // Get the updated Cosmos SQL DB from k8s + namespacedName := types.NamespacedName{Name: name, Namespace: namespace} + err := tc.k8sClient.Get(ctx, namespacedName, sqlDBInstance) + assert.Equal(nil, err, "getting cosmos SQL DB from k8s") + + // Update the Cosmos SQL DB throughput (we expect that this will be rejected by Cosmos DB) + var throughPutRUs int32 = 1000 + sqlDBInstance.Spec.Throughput = &throughPutRUs + err = tc.k8sClient.Update(ctx, sqlDBInstance) + assert.Equal(nil, err, "updating cosmos sql DB in k8s") + + // Ensure we get an error eventually + assert.Eventually(func() bool { + updatedInstance := &v1alpha1.CosmosDBSQLDatabase{} + err = tc.k8sClient.Get(ctx, namespacedName, updatedInstance) + assert.Equal(nil, err, "err getting Cosmos SQL DB from k8s") + + return updatedInstance.Status.Provisioned == false && + updatedInstance.Status.FailedProvisioning == true && + strings.Contains(updatedInstance.Status.Message, "Throughput update is not supported for resources created without dedicated throughput") + }, tc.timeout, tc.retry, "wait for sql database to be updated in k8s") + + EnsureDelete(ctx, t, tc, sqlDBInstance) +} + +func doesThroughputMatch( + ctx context.Context, + client documentdb.SQLResourcesClient, + rgName string, + cosmosDBAccountName string, + dbName string, + expectedThroughput int32) (bool, error) { + + throughputSettings, err := client.GetSQLDatabaseThroughput(ctx, rgName, cosmosDBAccountName, dbName) + if err != nil { + return false, errors.Wrap(err, "err getting cosmos sql DB throughput from Azure") + } + + if throughputSettings.ThroughputSettingsGetProperties == nil { + return false, nil + } + + if throughputSettings.ThroughputSettingsGetProperties.Resource == nil { + return false, nil + } + + if throughputSettings.ThroughputSettingsGetProperties.Resource.Throughput == nil { + return false, nil + } + + return *throughputSettings.ThroughputSettingsGetProperties.Resource.Throughput == expectedThroughput, nil +} + +func TestCosmosDBSQLDatabaseNoCosmosDBAccount(t *testing.T) { + t.Parallel() + defer PanicRecover(t) + ctx := context.Background() + + //wrong resource group name + resourceGroupName := "gone" + + name := GenerateTestResourceNameWithRandom("cosmosdbsql", 8) + namespace := "default" + + sqlDBInstance := &v1alpha1.CosmosDBSQLDatabase{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: v1alpha1.CosmosDBSQLDatabaseSpec{ + ResourceGroup: resourceGroupName, + Account: name, + }, + } + + EnsureInstanceWithResult(ctx, t, tc, sqlDBInstance, errhelp.ResourceGroupNotFoundErrorCode, false) + EnsureDelete(ctx, t, tc, sqlDBInstance) +} diff --git a/controllers/helpers.go b/controllers/helpers.go index 1996fd19409..76a806d1039 100644 --- a/controllers/helpers.go +++ b/controllers/helpers.go @@ -16,8 +16,8 @@ import ( "github.com/Azure/azure-sdk-for-go/services/keyvault/mgmt/2018-02-14/keyvault" "github.com/Azure/go-autorest/autorest/to" "github.com/go-logr/logr" - "github.com/pkg/errors" uuid "github.com/gofrs/uuid" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/crypto/ssh" diff --git a/controllers/suite_test.go b/controllers/suite_test.go index ad86015d96f..e02f59e9840 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -38,7 +38,8 @@ import ( resourcemanagersqluser "github.com/Azure/azure-service-operator/pkg/resourcemanager/azuresql/azuresqluser" resourcemanagersqlvnetrule "github.com/Azure/azure-service-operator/pkg/resourcemanager/azuresql/azuresqlvnetrule" resourcemanagerconfig "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" - resourcemanagercosmosdb "github.com/Azure/azure-service-operator/pkg/resourcemanager/cosmosdbs" + resourcemanagercosmosdbaccount "github.com/Azure/azure-service-operator/pkg/resourcemanager/cosmosdb/account" + resourcemanagercosmosdbsqldatabase "github.com/Azure/azure-service-operator/pkg/resourcemanager/cosmosdb/sqldatabase" resourcemanagereventhub "github.com/Azure/azure-service-operator/pkg/resourcemanager/eventhubs" resourcemanagerkeyvaults "github.com/Azure/azure-service-operator/pkg/resourcemanager/keyvaults" "github.com/Azure/azure-service-operator/pkg/resourcemanager/loadbalancer" @@ -266,7 +267,7 @@ func setup() error { err = (&CosmosDBReconciler{ Reconciler: &AsyncReconciler{ Client: k8sManager.GetClient(), - AzureClient: resourcemanagercosmosdb.NewAzureCosmosDBManager(config.GlobalCredentials(), secretClient), + AzureClient: resourcemanagercosmosdbaccount.NewAzureCosmosDBManager(config.GlobalCredentials(), secretClient), Telemetry: telemetry.InitializeTelemetryDefault( "CosmosDB", ctrl.Log.WithName("controllers").WithName("CosmosDB"), @@ -279,6 +280,22 @@ func setup() error { return err } + err = (&CosmosDBSQLDatabaseReconciler{ + Reconciler: &AsyncReconciler{ + Client: k8sManager.GetClient(), + AzureClient: resourcemanagercosmosdbsqldatabase.NewAzureCosmosDBSQLDatabaseManager(config.GlobalCredentials()), + Telemetry: telemetry.InitializeTelemetryDefault( + "CosmosDBSQLDatabase", + ctrl.Log.WithName("controllers").WithName("CosmosDBSQLDatabase"), + ), + Recorder: k8sManager.GetEventRecorderFor("CosmosDBSQLDatabase-controller"), + Scheme: scheme.Scheme, + }, + }).SetupWithManager(k8sManager) + if err != nil { + return err + } + err = (&EventhubReconciler{ Reconciler: &AsyncReconciler{ Client: k8sManager.GetClient(), diff --git a/docs/services/cosmosdb/cosmosdb.md b/docs/services/cosmosdb/cosmosdb.md index cc04cd91088..6b7a66af4b5 100755 --- a/docs/services/cosmosdb/cosmosdb.md +++ b/docs/services/cosmosdb/cosmosdb.md @@ -2,5 +2,7 @@ ## Resources supported -The Cosmos DB operator can be used to provision a CosmosDB instance given the Cosmos DB instance type, location, and resource group. - 1. [Sample YAML file](/config/samples/azure_v1alpha1_cosmosdb.yaml) +| Resource | Description | Sample YAML | +|-----------------------------------|-------------------------------------------------------------------------------|------------------------------------------------------------------------| +| CosmosDB | A Cosmos DB account. | [Sample](/config/samples/azure_v1alpha1_cosmosdb.yaml) | +| CosmosDBSQLDatabase | A Cosmos DB SQL Database for storing data | [Sample](/config/samples/azure_v1alpha1_cosmosdbsqldatabase.yaml) | diff --git a/main.go b/main.go index 68a885e8d4d..92132541272 100644 --- a/main.go +++ b/main.go @@ -37,7 +37,8 @@ import ( resourcemanagersqlvnetrule "github.com/Azure/azure-service-operator/pkg/resourcemanager/azuresql/azuresqlvnetrule" "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" resourcemanagerconfig "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" - resourcemanagercosmosdb "github.com/Azure/azure-service-operator/pkg/resourcemanager/cosmosdbs" + resourcemanagercosmosdbaccount "github.com/Azure/azure-service-operator/pkg/resourcemanager/cosmosdb/account" + resourcemanagercosmosdbsqldatabase "github.com/Azure/azure-service-operator/pkg/resourcemanager/cosmosdb/sqldatabase" resourcemanagereventhub "github.com/Azure/azure-service-operator/pkg/resourcemanager/eventhubs" resourcemanagerkeyvault "github.com/Azure/azure-service-operator/pkg/resourcemanager/keyvaults" loadbalancer "github.com/Azure/azure-service-operator/pkg/resourcemanager/loadbalancer" @@ -179,10 +180,11 @@ func main() { ) eventhubNamespaceClient := resourcemanagereventhub.NewEventHubNamespaceClient(config.GlobalCredentials()) consumerGroupClient := resourcemanagereventhub.NewConsumerGroupClient(config.GlobalCredentials()) - cosmosDBClient := resourcemanagercosmosdb.NewAzureCosmosDBManager( + cosmosDBClient := resourcemanagercosmosdbaccount.NewAzureCosmosDBManager( config.GlobalCredentials(), secretClient, ) + cosmosDBSQLDatabaseClient := resourcemanagercosmosdbsqldatabase.NewAzureCosmosDBSQLDatabaseManager(config.GlobalCredentials()) keyVaultManager := resourcemanagerkeyvault.NewAzureKeyVaultManager(config.GlobalCredentials(), mgr.GetScheme()) keyVaultKeyManager := resourcemanagerkeyvault.NewKeyvaultKeyClient(config.GlobalCredentials(), keyVaultManager) eventhubClient := resourcemanagereventhub.NewEventhubClient(config.GlobalCredentials(), secretClient, scheme) @@ -252,6 +254,23 @@ func main() { os.Exit(1) } + err = (&controllers.CosmosDBSQLDatabaseReconciler{ + Reconciler: &controllers.AsyncReconciler{ + Client: mgr.GetClient(), + AzureClient: cosmosDBSQLDatabaseClient, + Telemetry: telemetry.InitializeTelemetryDefault( + "CosmosDBSQLDatabase", + ctrl.Log.WithName("controllers").WithName("CosmosDBSQLDatabase"), + ), + Recorder: mgr.GetEventRecorderFor("CosmosDBSQLDatabase-controller"), + Scheme: scheme, + }, + }).SetupWithManager(mgr) + if err != nil { + setupLog.Error(err, "unable to create controller", "controller", "CosmosDBSQLDatabase") + os.Exit(1) + } + err = (&controllers.RedisCacheReconciler{ Reconciler: &controllers.AsyncReconciler{ Client: mgr.GetClient(), diff --git a/pkg/errhelp/errhelp.go b/pkg/errhelp/errhelp.go index 8d9e1968d84..d3e94929178 100644 --- a/pkg/errhelp/errhelp.go +++ b/pkg/errhelp/errhelp.go @@ -43,7 +43,16 @@ func StripErrorTimes(err string) string { } -func HandleEnsureError(err error, allowedErrorTypes []string, unrecoverableErrorTypes []string) (bool, error) { +// IsErrorFatal checks the given error against the provided list of allowed and unrecoverable error types. +// - Allowed errors are NOT fatal and no error is returned. When returned to the async_reconciler this means that +// reconciliation is reattempted. +// - Unrecoverable errors are fatal. When returned to the async_reconciler reconciliation is stopped until +// a new modification is made to the resource in question. This is useful for things like client errors that +// no amount of reconciliation will fix. +// If an error is not in the allowed list and also not in the unrecoverable list, it is classified as nonfatal, +// but an error is returned. When returned to the async_reconciler, reconciliation will continue but an error will +// be logged. +func IsErrorFatal(err error, allowedErrorTypes []string, unrecoverableErrorTypes []string) (bool, error) { azerr := NewAzureError(err) if helpers.ContainsString(allowedErrorTypes, azerr.Type) { return false, nil // false means the resource is not in a terminal state yet, keep trying to reconcile. diff --git a/pkg/resourcemanager/azuresql/azuresqlfailovergroup/azuresqlfailovergroup_reconcile.go b/pkg/resourcemanager/azuresql/azuresqlfailovergroup/azuresqlfailovergroup_reconcile.go index f0dee68c293..0272ab37cf6 100644 --- a/pkg/resourcemanager/azuresql/azuresqlfailovergroup/azuresqlfailovergroup_reconcile.go +++ b/pkg/resourcemanager/azuresql/azuresqlfailovergroup/azuresqlfailovergroup_reconcile.go @@ -7,6 +7,7 @@ import ( "context" "fmt" + "github.com/Azure/azure-service-operator/api" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -39,7 +40,7 @@ func (fg *AzureSqlFailoverGroupManager) Ensure(ctx context.Context, obj runtime. } pClient := pollclient.NewPollClient(fg.Creds) - lroPollResult, err := pClient.PollLongRunningOperationIfNeeded(ctx, &instance.Status) + lroPollResult, err := pClient.PollLongRunningOperationIfNeeded(ctx, &instance.Status, api.PollingURLKindCreateOrUpdate) if err != nil { instance.Status.Message = err.Error() return false, err @@ -48,6 +49,11 @@ func (fg *AzureSqlFailoverGroupManager) Ensure(ctx context.Context, obj runtime. // Need to wait a bit before trying again return false, nil } + if lroPollResult == pollclient.PollResultBadRequest { + // Reached a terminal state + instance.Status.SetFailedProvisioning(instance.Status.Message) + return true, nil + } failoverGroupsClient, err := azuresqlshared.GetGoFailoverGroupsClient(fg.Creds) if err != nil { @@ -67,14 +73,14 @@ func (fg *AzureSqlFailoverGroupManager) Ensure(ctx context.Context, obj runtime. azerr := errhelp.NewAzureError(err) if azerr.Type != errhelp.ResourceNotFound { instance.Status.Message = fmt.Sprintf("AzureSqlFailoverGroup Get error %s", err.Error()) - return errhelp.HandleEnsureError(err, notFoundErrorCodes, nil) + return errhelp.IsErrorFatal(err, notFoundErrorCodes, nil) } } failoverGroupProperties, err := fg.TransformToSQLFailoverGroup(ctx, instance) if err != nil { instance.Status.Message = err.Error() - return errhelp.HandleEnsureError(err, notFoundErrorCodes, nil) + return errhelp.IsErrorFatal(err, notFoundErrorCodes, nil) } // We found a failover group, check to make sure that it matches what we have locally @@ -127,10 +133,10 @@ func (fg *AzureSqlFailoverGroupManager) Ensure(ctx context.Context, obj runtime. unrecoverableErrors := []string{ errhelp.InvalidFailoverGroupRegion, } - return errhelp.HandleEnsureError(err, append(allowedErrors, notFoundErrorCodes...), unrecoverableErrors) + return errhelp.IsErrorFatal(err, append(allowedErrors, notFoundErrorCodes...), unrecoverableErrors) } instance.Status.SetProvisioning("Resource request successfully submitted to Azure") - instance.Status.PollingURL = future.PollingURL() + instance.Status.SetPollingURL(future.PollingURL(), api.PollingURLKindCreateOrUpdate) // Need to poll the polling URL, so not done yet! return false, nil diff --git a/pkg/resourcemanager/cosmosdbs/cosmosdb.go b/pkg/resourcemanager/cosmosdb/account/cosmosdb.go similarity index 82% rename from pkg/resourcemanager/cosmosdbs/cosmosdb.go rename to pkg/resourcemanager/cosmosdb/account/cosmosdb.go index 3048c191523..8b64c10b290 100644 --- a/pkg/resourcemanager/cosmosdbs/cosmosdb.go +++ b/pkg/resourcemanager/cosmosdb/account/cosmosdb.go @@ -1,21 +1,21 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -package cosmosdbs +package account import ( "context" "fmt" "net/http" - "strings" - "github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2015-04-08/documentdb" + "github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2021-03-15/documentdb" + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/to" + "github.com/Azure/azure-service-operator/api/v1alpha1" "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" - "github.com/Azure/azure-service-operator/pkg/resourcemanager/iam" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/cosmosdb" "github.com/Azure/azure-service-operator/pkg/secrets" - "github.com/Azure/go-autorest/autorest" - "github.com/Azure/go-autorest/autorest/to" ) // AzureCosmosDBManager is the struct which contains helper functions for resource groups @@ -24,27 +24,13 @@ type AzureCosmosDBManager struct { SecretClient secrets.SecretClient } -func getCosmosDBClient(creds config.Credentials) (documentdb.DatabaseAccountsClient, error) { - cosmosDBClient := documentdb.NewDatabaseAccountsClientWithBaseURI(config.BaseURI(), creds.SubscriptionID()) - - a, err := iam.GetResourceManagementAuthorizer(creds) - if err != nil { - cosmosDBClient = documentdb.DatabaseAccountsClient{} - } else { - cosmosDBClient.Authorizer = a - } - - err = cosmosDBClient.AddToUserAgent(config.UserAgent()) - return cosmosDBClient, err -} - // CreateOrUpdateCosmosDB creates a new CosmosDB func (m *AzureCosmosDBManager) CreateOrUpdateCosmosDB( ctx context.Context, accountName string, spec v1alpha1.CosmosDBSpec, - tags map[string]*string) (*documentdb.DatabaseAccount, string, error) { - cosmosDBClient, err := getCosmosDBClient(m.Creds) + tags map[string]*string) (*documentdb.DatabaseAccountGetResults, string, error) { + cosmosDBClient, err := cosmosdb.GetCosmosDBAccountClient(m.Creds) if err != nil { return nil, "", err } @@ -61,7 +47,7 @@ func (m *AzureCosmosDBManager) CreateOrUpdateCosmosDB( EnableMultipleWriteLocations: &spec.Properties.EnableMultipleWriteLocations, Locations: getLocations(spec), Capabilities: getCapabilities(spec), - IPRangeFilter: getIPRangeFilter(spec), + IPRules: getIPRules(spec), }, } createUpdateFuture, err := cosmosDBClient.CreateOrUpdate( @@ -83,8 +69,8 @@ func (m *AzureCosmosDBManager) CreateOrUpdateCosmosDB( func (m *AzureCosmosDBManager) GetCosmosDB( ctx context.Context, groupName string, - cosmosDBName string) (*documentdb.DatabaseAccount, error) { - cosmosDBClient, err := getCosmosDBClient(m.Creds) + cosmosDBName string) (*documentdb.DatabaseAccountGetResults, error) { + cosmosDBClient, err := cosmosdb.GetCosmosDBAccountClient(m.Creds) if err != nil { return nil, err } @@ -100,7 +86,7 @@ func (m *AzureCosmosDBManager) GetCosmosDB( func (m *AzureCosmosDBManager) CheckNameExistsCosmosDB( ctx context.Context, accountName string) (bool, error) { - cosmosDBClient, err := getCosmosDBClient(m.Creds) + cosmosDBClient, err := cosmosdb.GetCosmosDBAccountClient(m.Creds) if err != nil { return false, err } @@ -125,7 +111,7 @@ func (m *AzureCosmosDBManager) DeleteCosmosDB( ctx context.Context, groupName string, cosmosDBName string) (*autorest.Response, error) { - cosmosDBClient, err := getCosmosDBClient(m.Creds) + cosmosDBClient, err := cosmosdb.GetCosmosDBAccountClient(m.Creds) if err != nil { return nil, err } @@ -147,7 +133,7 @@ func (m *AzureCosmosDBManager) ListKeys( ctx context.Context, groupName string, accountName string) (*documentdb.DatabaseAccountListKeysResult, error) { - client, err := getCosmosDBClient(m.Creds) + client, err := cosmosdb.GetCosmosDBAccountClient(m.Creds) if err != nil { return nil, err } @@ -165,7 +151,7 @@ func (m *AzureCosmosDBManager) ListConnectionStrings( ctx context.Context, groupName string, accountName string) (*documentdb.DatabaseAccountListConnectionStringsResult, error) { - client, err := getCosmosDBClient(m.Creds) + client, err := cosmosdb.GetCosmosDBAccountClient(m.Creds) if err != nil { return nil, err } @@ -241,10 +227,15 @@ func getCapabilities(spec v1alpha1.CosmosDBSpec) *[]documentdb.Capability { return &capabilities } -func getIPRangeFilter(spec v1alpha1.CosmosDBSpec) *string { - sIPRules := "" - if spec.IPRules != nil { - sIPRules = strings.Join(*spec.IPRules, ",") +func getIPRules(spec v1alpha1.CosmosDBSpec) *[]documentdb.IPAddressOrRange { + if spec.IPRules == nil { + return nil } - return &sIPRules + + var result []documentdb.IPAddressOrRange + for _, rule := range *spec.IPRules { + result = append(result, documentdb.IPAddressOrRange{IPAddressOrRange: &rule}) + } + + return &result } diff --git a/pkg/resourcemanager/cosmosdbs/cosmosdb_manager.go b/pkg/resourcemanager/cosmosdb/account/cosmosdb_manager.go similarity index 89% rename from pkg/resourcemanager/cosmosdbs/cosmosdb_manager.go rename to pkg/resourcemanager/cosmosdb/account/cosmosdb_manager.go index ec53a6b16c4..dc358bb5987 100644 --- a/pkg/resourcemanager/cosmosdbs/cosmosdb_manager.go +++ b/pkg/resourcemanager/cosmosdb/account/cosmosdb_manager.go @@ -1,17 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -package cosmosdbs +package account import ( "context" - "github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2015-04-08/documentdb" + "github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2021-03-15/documentdb" + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/azure-service-operator/api/v1alpha1" "github.com/Azure/azure-service-operator/pkg/resourcemanager" "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" "github.com/Azure/azure-service-operator/pkg/secrets" - "github.com/Azure/go-autorest/autorest" ) // NewAzureCosmosDBManager creates a new cosmos db client @@ -25,10 +26,10 @@ func NewAzureCosmosDBManager(creds config.Credentials, secretClient secrets.Secr // CosmosDBManager client functions type CosmosDBManager interface { // CreateOrUpdateCosmosDB creates a new cosmos database account - CreateOrUpdateCosmosDB(ctx context.Context, cosmosDBName string, spec v1alpha1.CosmosDBSpec, tags map[string]*string) (*documentdb.DatabaseAccount, string, error) + CreateOrUpdateCosmosDB(ctx context.Context, cosmosDBName string, spec v1alpha1.CosmosDBSpec, tags map[string]*string) (*documentdb.DatabaseAccountGetProperties, string, error) // GetCosmosDB gets a cosmos database account - GetCosmosDB(ctx context.Context, groupName string, cosmosDBName string) (*documentdb.DatabaseAccount, error) + GetCosmosDB(ctx context.Context, groupName string, cosmosDBName string) (*documentdb.DatabaseAccountGetProperties, error) // DeleteCosmosDB removes the cosmos database account DeleteCosmosDB(ctx context.Context, groupName string, cosmosDBName string) (*autorest.Response, error) diff --git a/pkg/resourcemanager/cosmosdbs/cosmosdb_reconcile.go b/pkg/resourcemanager/cosmosdb/account/cosmosdb_reconcile.go similarity index 96% rename from pkg/resourcemanager/cosmosdbs/cosmosdb_reconcile.go rename to pkg/resourcemanager/cosmosdb/account/cosmosdb_reconcile.go index ac11d81824d..b05f3eeaa6f 100644 --- a/pkg/resourcemanager/cosmosdbs/cosmosdb_reconcile.go +++ b/pkg/resourcemanager/cosmosdb/account/cosmosdb_reconcile.go @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -package cosmosdbs +package account import ( "context" "fmt" "strings" - "github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2015-04-08/documentdb" + "github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2021-03-15/documentdb" "github.com/Azure/azure-service-operator/api/v1alpha1" "github.com/Azure/azure-service-operator/pkg/errhelp" @@ -251,7 +251,7 @@ func (m *AzureCosmosDBManager) convert(obj runtime.Object) (*v1alpha1.CosmosDB, return db, nil } -func (m *AzureCosmosDBManager) createOrUpdateSecret(ctx context.Context, secretClient secrets.SecretClient, instance *v1alpha1.CosmosDB, db *documentdb.DatabaseAccount) error { +func (m *AzureCosmosDBManager) createOrUpdateSecret(ctx context.Context, secretClient secrets.SecretClient, instance *v1alpha1.CosmosDB, db *documentdb.DatabaseAccountGetResults) error { connStrResult, err := m.ListConnectionStrings(ctx, instance.Spec.ResourceGroup, instance.ObjectMeta.Name) if err != nil { return err @@ -279,8 +279,8 @@ func (m *AzureCosmosDBManager) createOrUpdateSecret(ctx context.Context, secretC } // set each location's endpoint in the secret - if db.DatabaseAccountProperties.ReadLocations != nil { - for _, l := range *db.DatabaseAccountProperties.ReadLocations { + if db.DatabaseAccountGetProperties.ReadLocations != nil { + for _, l := range *db.DatabaseAccountGetProperties.ReadLocations { safeLocationName := helpers.RemoveNonAlphaNumeric(strings.ToLower(*l.LocationName)) secretData[safeLocationName+"Endpoint"] = []byte(*l.DocumentEndpoint) } diff --git a/pkg/resourcemanager/cosmosdb/common.go b/pkg/resourcemanager/cosmosdb/common.go new file mode 100644 index 00000000000..88fa4956b0d --- /dev/null +++ b/pkg/resourcemanager/cosmosdb/common.go @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package cosmosdb + +import ( + "github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2021-03-15/documentdb" + + "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/iam" +) + +func GetCosmosDBAccountClient(creds config.Credentials) (documentdb.DatabaseAccountsClient, error) { + client := documentdb.NewDatabaseAccountsClientWithBaseURI(config.BaseURI(), creds.SubscriptionID()) + + a, err := iam.GetResourceManagementAuthorizer(creds) + if err != nil { + return documentdb.DatabaseAccountsClient{}, err + } + + client.Authorizer = a + + err = client.AddToUserAgent(config.UserAgent()) + return client, err +} + +func GetCosmosDBSQLDatabaseClient(creds config.Credentials) (documentdb.SQLResourcesClient, error) { + client := documentdb.NewSQLResourcesClientWithBaseURI(config.BaseURI(), creds.SubscriptionID()) + + a, err := iam.GetResourceManagementAuthorizer(creds) + if err != nil { + return documentdb.SQLResourcesClient{}, err + } + + client.Authorizer = a + + err = client.AddToUserAgent(config.UserAgent()) + return client, err +} diff --git a/pkg/resourcemanager/cosmosdb/sqldatabase/cosmosdbsql.go b/pkg/resourcemanager/cosmosdb/sqldatabase/cosmosdbsql.go new file mode 100644 index 00000000000..619f8788279 --- /dev/null +++ b/pkg/resourcemanager/cosmosdb/sqldatabase/cosmosdbsql.go @@ -0,0 +1,294 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package sqldatabase + +import ( + "context" + "fmt" + + "github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2021-03-15/documentdb" + "github.com/Azure/azure-service-operator/api" + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + + "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/pkg/errhelp" + "github.com/Azure/azure-service-operator/pkg/helpers" + "github.com/Azure/azure-service-operator/pkg/resourcemanager" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/cosmosdb" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/pollclient" +) + +func NewAzureCosmosDBSQLDatabaseManager(creds config.Credentials) *AzureCosmosDBSQLDatabaseManager { + return &AzureCosmosDBSQLDatabaseManager{ + Creds: creds, + } +} + +var _ resourcemanager.ARMClient = &AzureCosmosDBSQLDatabaseManager{} + +type AzureCosmosDBSQLDatabaseManager struct { + Creds config.Credentials +} + +/* + * ARMClient implementation + */ + +func (m *AzureCosmosDBSQLDatabaseManager) Ensure(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + instance, err := m.convert(obj) + if err != nil { + return false, err + } + + cosmosDBSQLDatabaseClient, err := cosmosdb.GetCosmosDBSQLDatabaseClient(m.Creds) + if err != nil { + return false, errors.Wrapf(err, "failed to create cosmos DB SQL database client") + } + + pClient := pollclient.NewPollClient(m.Creds) + lroPollResult, err := pClient.PollLongRunningOperationIfNeededV1Alpha1(ctx, &instance.Status, api.PollingURLKindCreateOrUpdate) + if err != nil { + instance.Status.Message = err.Error() + return false, err + } + + if lroPollResult == pollclient.PollResultTryAgainLater { + // Need to wait a bit before trying again + return false, nil + } + if lroPollResult == pollclient.PollResultBadRequest { + // Reached a terminal state + instance.Status.SetFailedProvisioning(instance.Status.Message) + return true, nil + } + + notFoundErrorCodes := []string{ + errhelp.ParentNotFoundErrorCode, + errhelp.ResourceGroupNotFoundErrorCode, + errhelp.NotFoundErrorCode, + errhelp.ResourceNotFound, + } + + // Get the existing resource + sqlDatabase, err := cosmosDBSQLDatabaseClient.GetSQLDatabase( + ctx, + instance.Spec.ResourceGroup, + instance.Spec.Account, + instance.ObjectMeta.Name) + if err != nil { + azerr := errhelp.NewAzureError(err) + if azerr.Type != errhelp.ResourceNotFound && azerr.Type != errhelp.NotFoundErrorCode { + instance.Status.Message = fmt.Sprintf("failed GET-ing CosmosDBSQLDatabase: %s", err.Error()) + return errhelp.IsErrorFatal(err, notFoundErrorCodes, nil) + } + } + + // TODO: There's a bug in Cosmos DB SQL Database where the throughput settings configured on + // TODO: the DB don't actually get shown on the DB, instead they are shown on the throughput child resource. + // TODO: We monitor that resource too so we can diff against it + sqlDatabaseThroughputSettings, err := cosmosDBSQLDatabaseClient.GetSQLDatabaseThroughput( + ctx, + instance.Spec.ResourceGroup, + instance.Spec.Account, + instance.ObjectMeta.Name) + if err != nil { + azerr := errhelp.NewAzureError(err) + if azerr.Type != errhelp.ResourceNotFound && azerr.Type != errhelp.NotFoundErrorCode { + instance.Status.Message = fmt.Sprintf("failed GET-ing CosmosDBSQLDatabaseThroughput: %s", err.Error()) + return errhelp.IsErrorFatal(err, notFoundErrorCodes, nil) + } + } + + // Transform our spec to Azure shape + transformed := m.transformToCosmosDBSQLDatabase(instance) + + // Check if we're in the goal state already + resourceMatchesAzure := DoesResourceMatchAzure(transformed, sqlDatabase, sqlDatabaseThroughputSettings) + if resourceMatchesAzure { + instance.Status.SetProvisioned(resourcemanager.SuccessMsg) + instance.Status.ResourceId = *sqlDatabase.ID + return true, nil + } + + // Drive to goal state + instance.Status.SetProvisioning("") + future, err := cosmosDBSQLDatabaseClient.CreateUpdateSQLDatabase( + ctx, + instance.Spec.ResourceGroup, + instance.Spec.Account, + instance.ObjectMeta.Name, + transformed) + if err != nil { + instance.Status.Message = err.Error() + allowedErrors := []string{ + errhelp.AsyncOpIncompleteError, + errhelp.AlreadyExists, + errhelp.FailoverGroupBusy, + } + unrecoverableErrors := []string{ + errhelp.InvalidFailoverGroupRegion, + } + return errhelp.IsErrorFatal(err, append(allowedErrors, notFoundErrorCodes...), unrecoverableErrors) + } + instance.Status.SetProvisioning("Resource request successfully submitted to Azure") + instance.Status.SetPollingURL(future.PollingURL(), api.PollingURLKindCreateOrUpdate) + + // Need to poll the polling URL, so not done yet! + return false, nil +} + +func (m *AzureCosmosDBSQLDatabaseManager) Delete(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + instance, err := m.convert(obj) + if err != nil { + return false, err + } + + cosmosDBSQLDatabaseClient, err := cosmosdb.GetCosmosDBSQLDatabaseClient(m.Creds) + if err != nil { + return false, errors.Wrapf(err, "failed to create cosmos DB SQL database client") + } + + // TODO: What if we have a pollURL already but it's for a create + + pClient := pollclient.NewPollClient(m.Creds) + lroPollResult, err := pClient.PollLongRunningOperationIfNeededV1Alpha1(ctx, &instance.Status, api.PollingURLKindDelete) + if err != nil { + instance.Status.Message = err.Error() + return false, err + } + + switch lroPollResult { + case pollclient.PollResultTryAgainLater: + // Need to wait a bit before trying again + return true, nil + case pollclient.PollResultBadRequest: + // Failed to delete + instance.Status.SetFailedProvisioning(instance.Status.Message) + return true, nil + case pollclient.PollResultCompletedSuccessfully: + // Deleted + return false, nil + case pollclient.PollResultNoPollingNeeded: + // Continue on to issue new delete + default: + return true, errors.Errorf("unknown polling result %q", lroPollResult) + } + + future, err := cosmosDBSQLDatabaseClient.DeleteSQLDatabase(ctx, instance.Spec.ResourceGroup, instance.Spec.Account, instance.ObjectMeta.Name) + if err != nil { + instance.Status.Message = err.Error() + azerr := errhelp.NewAzureError(err) + + // these errors are expected + ignore := []string{ + errhelp.AsyncOpIncompleteError, + } + + // this means the thing doesn't exist + finished := []string{ + errhelp.ResourceNotFound, + errhelp.ResourceGroupNotFoundErrorCode, + } + + if helpers.ContainsString(ignore, azerr.Type) { + return true, nil + } + + if helpers.ContainsString(finished, azerr.Type) { + return false, nil + } + instance.Status.Message = fmt.Sprintf("delete failed with: %s", err.Error()) + return false, err + } + + // Not done yet, keep trying + instance.Status.SetPollingURL(future.PollingURL(), api.PollingURLKindDelete) + + return true, nil +} + +func (m *AzureCosmosDBSQLDatabaseManager) GetParents(obj runtime.Object) ([]resourcemanager.KubeParent, error) { + instance, err := m.convert(obj) + if err != nil { + return nil, err + } + + return []resourcemanager.KubeParent{ + { + Key: types.NamespacedName{ + Namespace: instance.Namespace, + Name: instance.Spec.Account, + }, + Target: &v1alpha1.CosmosDB{}, + }, + { + Key: types.NamespacedName{ + Namespace: instance.Namespace, + Name: instance.Spec.ResourceGroup, + }, + Target: &v1alpha1.ResourceGroup{}, + }, + }, nil +} + +func (m *AzureCosmosDBSQLDatabaseManager) GetStatus(obj runtime.Object) (*v1alpha1.ASOStatus, error) { + instance, err := m.convert(obj) + if err != nil { + return nil, err + } + + return &instance.Status, nil +} + +/* + * Helper functions + */ + +func (m *AzureCosmosDBSQLDatabaseManager) convert(obj runtime.Object) (*v1alpha1.CosmosDBSQLDatabase, error) { + db, ok := obj.(*v1alpha1.CosmosDBSQLDatabase) + if !ok { + return nil, fmt.Errorf("failed type assertion on kind: %s", obj.GetObjectKind().GroupVersionKind().String()) + } + return db, nil +} + +func makeTags(tags map[string]string) map[string]*string { + result := make(map[string]*string) + for key, value := range tags { + valCopy := value + result[key] = &valCopy + } + + return result +} + +func (m *AzureCosmosDBSQLDatabaseManager) transformToCosmosDBSQLDatabase(instance *v1alpha1.CosmosDBSQLDatabase) documentdb.SQLDatabaseCreateUpdateParameters { + var autoscaleSettings *documentdb.AutoscaleSettings + if instance.Spec.AutoscaleSettings != nil && instance.Spec.AutoscaleSettings.MaxThroughput != nil { + autoscaleSettings = &documentdb.AutoscaleSettings{ + MaxThroughput: instance.Spec.AutoscaleSettings.MaxThroughput, + } + } + + var createUpdateOptions *documentdb.CreateUpdateOptions + if instance.Spec.Throughput != nil || autoscaleSettings != nil { + createUpdateOptions = &documentdb.CreateUpdateOptions{ + Throughput: instance.Spec.Throughput, + AutoscaleSettings: autoscaleSettings, + } + } + + return documentdb.SQLDatabaseCreateUpdateParameters{ + Tags: makeTags(instance.Spec.Tags), + SQLDatabaseCreateUpdateProperties: &documentdb.SQLDatabaseCreateUpdateProperties{ + Resource: &documentdb.SQLDatabaseResource{ + ID: &instance.ObjectMeta.Name, + }, + Options: createUpdateOptions, + }, + } +} diff --git a/pkg/resourcemanager/cosmosdb/sqldatabase/resource_differ.go b/pkg/resourcemanager/cosmosdb/sqldatabase/resource_differ.go new file mode 100644 index 00000000000..a273e9daf38 --- /dev/null +++ b/pkg/resourcemanager/cosmosdb/sqldatabase/resource_differ.go @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package sqldatabase + +import "github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2021-03-15/documentdb" + +func doResourcesMatch(expected *documentdb.SQLDatabaseResource, actual *documentdb.SQLDatabaseGetPropertiesResource) bool { + var expectedIDCoalesced *string + if expected != nil { + expectedIDCoalesced = expected.ID + } + + var actualIDCoalesced *string + if actual != nil { + actualIDCoalesced = actual.ID + } + + if (expectedIDCoalesced == nil) != (actualIDCoalesced == nil) { + return false + } + if expectedIDCoalesced != nil && actualIDCoalesced != nil && *expectedIDCoalesced != *actualIDCoalesced { + return false + } + + return true +} + +// doesThroughputMatch checks if the throughput specified on an Azure SQL Cosmos DB matches the throughput settings on the +// child resource. We check the child resource because Cosmos DB has a bug where the throughput stuff doesn't get returned +// on a GET of the SQL DB itself. +func doesThroughputMatch(expected *documentdb.CreateUpdateOptions, actual *documentdb.ThroughputSettingsGetProperties) bool { + var expectedThroughputCoalesced *int32 + var expectedAutoscaleCoalesced *documentdb.AutoscaleSettings + if expected != nil { + expectedThroughputCoalesced = expected.Throughput + expectedAutoscaleCoalesced = expected.AutoscaleSettings + } + + var actualThroughputCoalesced *int32 + var actualAutoscaleCoalesced *documentdb.AutoscaleSettingsResource + if actual != nil && actual.Resource != nil { + actualThroughputCoalesced = actual.Resource.Throughput + actualAutoscaleCoalesced = actual.Resource.AutoscaleSettings + } + + // Throughput + if (expectedThroughputCoalesced == nil) != (actualThroughputCoalesced == nil) { + return false + } + if expectedThroughputCoalesced != nil && actualThroughputCoalesced != nil && + *expectedThroughputCoalesced != *actualThroughputCoalesced { + return false + } + + if !doAutoscaleSettingsMatch(expectedAutoscaleCoalesced, actualAutoscaleCoalesced) { + return false + } + + return true +} + +func doAutoscaleSettingsMatch(expected *documentdb.AutoscaleSettings, actual *documentdb.AutoscaleSettingsResource) bool { + var expectedMaxThroughputCoalesced *int32 + if expected != nil { + expectedMaxThroughputCoalesced = expected.MaxThroughput + } + + var actualMaxThroughputCoalesced *int32 + if actual != nil { + actualMaxThroughputCoalesced = actual.MaxThroughput + } + + if (expectedMaxThroughputCoalesced == nil) != (actualMaxThroughputCoalesced == nil) { + return false + } + + if expectedMaxThroughputCoalesced != nil && actualMaxThroughputCoalesced != nil { + if *expectedMaxThroughputCoalesced != *actualMaxThroughputCoalesced { + return false + } + } + + return true +} + +func doSQLDatabasePropertiesMatch(expected *documentdb.SQLDatabaseCreateUpdateProperties, actual *documentdb.SQLDatabaseGetProperties) bool { + var expectedResourceCoalesced *documentdb.SQLDatabaseResource + if expected != nil { + expectedResourceCoalesced = expected.Resource + } + + var actualResourceCoalesced *documentdb.SQLDatabaseGetPropertiesResource + if actual != nil { + actualResourceCoalesced = actual.Resource + } + + if !doResourcesMatch(expectedResourceCoalesced, actualResourceCoalesced) { + return false + } + + return true +} + +func doTagsMatch(expected map[string]*string, actual map[string]*string) bool { + if len(expected) != len(actual) { + return false + } + for expectedKey, expectedValue := range expected { + actualValue, ok := actual[expectedKey] + if !ok { + return false + } + + if (expectedValue == nil) != (actualValue == nil) { + return false + } + + if expectedValue != nil && actualValue != nil && *expectedValue != *actualValue { + return false + } + } + + return true +} + +func DoesResourceMatchAzure( + expected documentdb.SQLDatabaseCreateUpdateParameters, + actual documentdb.SQLDatabaseGetResults, + actualThroughput documentdb.ThroughputSettingsGetResults) bool { + + if !doTagsMatch(expected.Tags, actual.Tags) { + return false + } + + if !doSQLDatabasePropertiesMatch(expected.SQLDatabaseCreateUpdateProperties, actual.SQLDatabaseGetProperties) { + return false + } + + if expected.SQLDatabaseCreateUpdateProperties != nil { + if !doesThroughputMatch(expected.SQLDatabaseCreateUpdateProperties.Options, actualThroughput.ThroughputSettingsGetProperties) { + return false + } + } + + return true +} diff --git a/pkg/resourcemanager/cosmosdb/sqldatabase/resource_differ_test.go b/pkg/resourcemanager/cosmosdb/sqldatabase/resource_differ_test.go new file mode 100644 index 00000000000..49f4891d81c --- /dev/null +++ b/pkg/resourcemanager/cosmosdb/sqldatabase/resource_differ_test.go @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package sqldatabase_test + +import ( + "testing" + + "github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2021-03-15/documentdb" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/cosmosdb/sqldatabase" + "github.com/Azure/go-autorest/autorest/to" + . "github.com/onsi/gomega" +) + +func TestResourceDiffer(t *testing.T) { + testID := "test" + nonMatchingId := "nonmatching" + var throughput500 int32 = 500 + var throughput1000 int32 = 1000 + + cases := []struct { + name string + expected documentdb.SQLDatabaseCreateUpdateParameters + actual documentdb.SQLDatabaseGetResults + actualThroughput documentdb.ThroughputSettingsGetResults + equal bool + }{ + { + name: "empty types are equal", + expected: documentdb.SQLDatabaseCreateUpdateParameters{}, + actual: documentdb.SQLDatabaseGetResults{}, + actualThroughput: documentdb.ThroughputSettingsGetResults{}, + equal: true, + }, + { + name: "types with equal ids are equal", + expected: documentdb.SQLDatabaseCreateUpdateParameters{ + SQLDatabaseCreateUpdateProperties: &documentdb.SQLDatabaseCreateUpdateProperties{ + Resource: &documentdb.SQLDatabaseResource{ + ID: &testID, + }, + }, + }, + actual: documentdb.SQLDatabaseGetResults{ + SQLDatabaseGetProperties: &documentdb.SQLDatabaseGetProperties{ + Resource: &documentdb.SQLDatabaseGetPropertiesResource{ + ID: &testID, + }, + }, + }, + actualThroughput: documentdb.ThroughputSettingsGetResults{}, + equal: true, + }, + { + name: "types with equal empty ids are equal", + expected: documentdb.SQLDatabaseCreateUpdateParameters{ + SQLDatabaseCreateUpdateProperties: &documentdb.SQLDatabaseCreateUpdateProperties{ + Resource: &documentdb.SQLDatabaseResource{}, + }, + }, + actual: documentdb.SQLDatabaseGetResults{ + SQLDatabaseGetProperties: &documentdb.SQLDatabaseGetProperties{ + Resource: &documentdb.SQLDatabaseGetPropertiesResource{}, + }, + }, + actualThroughput: documentdb.ThroughputSettingsGetResults{}, + equal: true, + }, + { + name: "types with different ids are not equal", + expected: documentdb.SQLDatabaseCreateUpdateParameters{ + SQLDatabaseCreateUpdateProperties: &documentdb.SQLDatabaseCreateUpdateProperties{ + Resource: &documentdb.SQLDatabaseResource{ + ID: &testID, + }, + }, + }, + actual: documentdb.SQLDatabaseGetResults{ + SQLDatabaseGetProperties: &documentdb.SQLDatabaseGetProperties{ + Resource: &documentdb.SQLDatabaseGetPropertiesResource{ + ID: &nonMatchingId, + }, + }, + }, + actualThroughput: documentdb.ThroughputSettingsGetResults{}, + equal: false, + }, + { + name: "expected and actual mismatched throughput is ignored (throughput source of truth is from actualThroughput)", + expected: documentdb.SQLDatabaseCreateUpdateParameters{ + SQLDatabaseCreateUpdateProperties: &documentdb.SQLDatabaseCreateUpdateProperties{ + Options: &documentdb.CreateUpdateOptions{ + Throughput: &throughput500, + }, + }, + }, + actual: documentdb.SQLDatabaseGetResults{ + SQLDatabaseGetProperties: &documentdb.SQLDatabaseGetProperties{}, + }, + actualThroughput: documentdb.ThroughputSettingsGetResults{ + ThroughputSettingsGetProperties: &documentdb.ThroughputSettingsGetProperties{ + Resource: &documentdb.ThroughputSettingsGetPropertiesResource{ + Throughput: &throughput500, + }, + }, + }, + equal: true, + }, + { + name: "empty types, partially empty actualThroughput are equal", + expected: documentdb.SQLDatabaseCreateUpdateParameters{}, + actual: documentdb.SQLDatabaseGetResults{}, + actualThroughput: documentdb.ThroughputSettingsGetResults{ + ThroughputSettingsGetProperties: &documentdb.ThroughputSettingsGetProperties{}, + }, + equal: true, + }, + { + name: "empty types, partially empty actualThroughput are equal 2", + expected: documentdb.SQLDatabaseCreateUpdateParameters{}, + actual: documentdb.SQLDatabaseGetResults{}, + actualThroughput: documentdb.ThroughputSettingsGetResults{ + ThroughputSettingsGetProperties: &documentdb.ThroughputSettingsGetProperties{ + Resource: &documentdb.ThroughputSettingsGetPropertiesResource{}, + }, + }, + equal: true, + }, + { + name: "empty types, partially empty actualThroughput are equal 3", + expected: documentdb.SQLDatabaseCreateUpdateParameters{}, + actual: documentdb.SQLDatabaseGetResults{}, + actualThroughput: documentdb.ThroughputSettingsGetResults{ + ThroughputSettingsGetProperties: &documentdb.ThroughputSettingsGetProperties{ + Resource: &documentdb.ThroughputSettingsGetPropertiesResource{ + AutoscaleSettings: &documentdb.AutoscaleSettingsResource{}, + }, + }, + }, + equal: true, + }, + { + name: "empty throughput equal to nil throughput", + expected: documentdb.SQLDatabaseCreateUpdateParameters{ + SQLDatabaseCreateUpdateProperties: &documentdb.SQLDatabaseCreateUpdateProperties{ + Options: &documentdb.CreateUpdateOptions{}, + }, + }, + actual: documentdb.SQLDatabaseGetResults{}, + actualThroughput: documentdb.ThroughputSettingsGetResults{ + ThroughputSettingsGetProperties: &documentdb.ThroughputSettingsGetProperties{}, + }, + equal: true, + }, + { + name: "throughput not equal to nil throughput", + expected: documentdb.SQLDatabaseCreateUpdateParameters{ + SQLDatabaseCreateUpdateProperties: &documentdb.SQLDatabaseCreateUpdateProperties{ + Options: &documentdb.CreateUpdateOptions{ + Throughput: &throughput500, + }, + }, + }, + actual: documentdb.SQLDatabaseGetResults{}, + actualThroughput: documentdb.ThroughputSettingsGetResults{ + ThroughputSettingsGetProperties: &documentdb.ThroughputSettingsGetProperties{ + Resource: &documentdb.ThroughputSettingsGetPropertiesResource{}, + }, + }, + equal: false, + }, + { + name: "throughput not equal to different throughput", + expected: documentdb.SQLDatabaseCreateUpdateParameters{ + SQLDatabaseCreateUpdateProperties: &documentdb.SQLDatabaseCreateUpdateProperties{ + Options: &documentdb.CreateUpdateOptions{ + Throughput: &throughput500, + }, + }, + }, + actual: documentdb.SQLDatabaseGetResults{}, + actualThroughput: documentdb.ThroughputSettingsGetResults{ + ThroughputSettingsGetProperties: &documentdb.ThroughputSettingsGetProperties{ + Resource: &documentdb.ThroughputSettingsGetPropertiesResource{ + Throughput: &throughput1000, + }, + }, + }, + equal: false, + }, + { + name: "empty autoscale equal to nil autoscale", + expected: documentdb.SQLDatabaseCreateUpdateParameters{ + SQLDatabaseCreateUpdateProperties: &documentdb.SQLDatabaseCreateUpdateProperties{ + Options: &documentdb.CreateUpdateOptions{ + AutoscaleSettings: &documentdb.AutoscaleSettings{}, + }, + }, + }, + actual: documentdb.SQLDatabaseGetResults{}, + actualThroughput: documentdb.ThroughputSettingsGetResults{ + ThroughputSettingsGetProperties: &documentdb.ThroughputSettingsGetProperties{}, + }, + equal: true, + }, + { + name: "autoscale equal to same autoscale", + expected: documentdb.SQLDatabaseCreateUpdateParameters{ + SQLDatabaseCreateUpdateProperties: &documentdb.SQLDatabaseCreateUpdateProperties{ + Options: &documentdb.CreateUpdateOptions{ + AutoscaleSettings: &documentdb.AutoscaleSettings{ + MaxThroughput: &throughput500, + }, + }, + }, + }, + actual: documentdb.SQLDatabaseGetResults{}, + actualThroughput: documentdb.ThroughputSettingsGetResults{ + ThroughputSettingsGetProperties: &documentdb.ThroughputSettingsGetProperties{ + Resource: &documentdb.ThroughputSettingsGetPropertiesResource{ + AutoscaleSettings: &documentdb.AutoscaleSettingsResource{ + MaxThroughput: &throughput500, + }, + }, + }, + }, + equal: true, + }, + { + name: "autoscale not equal to different autoscale", + expected: documentdb.SQLDatabaseCreateUpdateParameters{ + SQLDatabaseCreateUpdateProperties: &documentdb.SQLDatabaseCreateUpdateProperties{ + Options: &documentdb.CreateUpdateOptions{ + AutoscaleSettings: &documentdb.AutoscaleSettings{ + MaxThroughput: &throughput500, + }, + }, + }, + }, + actual: documentdb.SQLDatabaseGetResults{}, + actualThroughput: documentdb.ThroughputSettingsGetResults{ + ThroughputSettingsGetProperties: &documentdb.ThroughputSettingsGetProperties{ + Resource: &documentdb.ThroughputSettingsGetPropertiesResource{ + AutoscaleSettings: &documentdb.AutoscaleSettingsResource{ + MaxThroughput: &throughput1000, + }, + }, + }, + }, + equal: false, + }, + { + name: "autoscale not equal to nil autoscale", + expected: documentdb.SQLDatabaseCreateUpdateParameters{ + SQLDatabaseCreateUpdateProperties: &documentdb.SQLDatabaseCreateUpdateProperties{ + Options: &documentdb.CreateUpdateOptions{ + AutoscaleSettings: &documentdb.AutoscaleSettings{ + MaxThroughput: &throughput500, + }, + }, + }, + }, + actual: documentdb.SQLDatabaseGetResults{}, + actualThroughput: documentdb.ThroughputSettingsGetResults{ + ThroughputSettingsGetProperties: &documentdb.ThroughputSettingsGetProperties{ + Resource: &documentdb.ThroughputSettingsGetPropertiesResource{}, + }, + }, + equal: false, + }, + { + name: "same tags equal", + expected: documentdb.SQLDatabaseCreateUpdateParameters{ + Tags: map[string]*string{ + "a": to.StringPtr("b"), + "c": to.StringPtr("d"), + }, + }, + actual: documentdb.SQLDatabaseGetResults{ + Tags: map[string]*string{ + "a": to.StringPtr("b"), + "c": to.StringPtr("d"), + }, + }, + actualThroughput: documentdb.ThroughputSettingsGetResults{}, + equal: true, + }, + { + name: "different tags not equal", + expected: documentdb.SQLDatabaseCreateUpdateParameters{ + Tags: map[string]*string{ + "a": to.StringPtr("b"), + "c": to.StringPtr("b"), + }, + }, + actual: documentdb.SQLDatabaseGetResults{ + Tags: map[string]*string{ + "a": to.StringPtr("b"), + "c": to.StringPtr("d"), + }, + }, + actualThroughput: documentdb.ThroughputSettingsGetResults{}, + equal: false, + }, + } + + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + g := NewGomegaWithT(t) + + equal := sqldatabase.DoesResourceMatchAzure(c.expected, c.actual, c.actualThroughput) + g.Expect(equal).To(Equal(c.equal)) + }) + } +} diff --git a/pkg/resourcemanager/eventhubs/namespace.go b/pkg/resourcemanager/eventhubs/namespace.go index ed1706acda9..df6d8342c1f 100644 --- a/pkg/resourcemanager/eventhubs/namespace.go +++ b/pkg/resourcemanager/eventhubs/namespace.go @@ -64,7 +64,7 @@ func (m *azureEventHubNamespaceManager) DeleteNamespace(ctx context.Context, res resourceGroupName, namespaceName) - if err != nil { + if err != nil { return autorest.Response{ Response: &http.Response{ StatusCode: 500, diff --git a/pkg/resourcemanager/keyvaults/keyvault.go b/pkg/resourcemanager/keyvaults/keyvault.go index 57add20f59c..2284a12290f 100644 --- a/pkg/resourcemanager/keyvaults/keyvault.go +++ b/pkg/resourcemanager/keyvaults/keyvault.go @@ -15,8 +15,8 @@ import ( kvops "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/to" - "github.com/pkg/errors" uuid "github.com/gofrs/uuid" + "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" diff --git a/pkg/resourcemanager/keyvaults/unittest/keyvault_test.go b/pkg/resourcemanager/keyvaults/unittest/keyvault_test.go index b85127e5867..62b26915c90 100644 --- a/pkg/resourcemanager/keyvaults/unittest/keyvault_test.go +++ b/pkg/resourcemanager/keyvaults/unittest/keyvault_test.go @@ -8,8 +8,8 @@ import ( "testing" keyvault "github.com/Azure/azure-sdk-for-go/services/keyvault/mgmt/2018-02-14/keyvault" - "github.com/google/go-cmp/cmp" uuid "github.com/gofrs/uuid" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" v1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" diff --git a/pkg/resourcemanager/pollclient/pollclient.go b/pkg/resourcemanager/pollclient/pollclient.go index 1f81d48bd95..d114fbd2276 100644 --- a/pkg/resourcemanager/pollclient/pollclient.go +++ b/pkg/resourcemanager/pollclient/pollclient.go @@ -7,6 +7,8 @@ import ( "context" "net/http" + "github.com/Azure/azure-service-operator/api" + "github.com/Azure/azure-service-operator/api/v1alpha1" "github.com/Azure/azure-service-operator/api/v1beta1" "github.com/Azure/azure-service-operator/pkg/errhelp" "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" @@ -134,20 +136,39 @@ const ( PollResultNoPollingNeeded = LongRunningOperationPollResult("noPollingNeeded") PollResultCompletedSuccessfully = LongRunningOperationPollResult("completedSuccessfully") PollResultTryAgainLater = LongRunningOperationPollResult("tryAgainLater") + PollResultBadRequest = LongRunningOperationPollResult("badRequest") ) -func (client PollClient) PollLongRunningOperationIfNeeded(ctx context.Context, status *v1beta1.ASOStatus) (LongRunningOperationPollResult, error) { +func (client PollClient) PollLongRunningOperationIfNeededV1Alpha1(ctx context.Context, status *v1alpha1.ASOStatus, kind api.PollingURLKind) (LongRunningOperationPollResult, error) { + wrapper := v1beta1.ASOStatus(*status) + result, err := client.PollLongRunningOperationIfNeeded(ctx, &wrapper, kind) + + // Propagate changes from wrapper to original type + status.PollingURL = wrapper.PollingURL + status.PollingURLKind = wrapper.PollingURLKind + status.Message = wrapper.Message + + return result, err +} + +func (client PollClient) PollLongRunningOperationIfNeeded(ctx context.Context, status *v1beta1.ASOStatus, kind api.PollingURLKind) (LongRunningOperationPollResult, error) { // Before we attempt to issue a new update, check if there is a previously ongoing update if status.PollingURL == "" { return PollResultNoPollingNeeded, nil } + // If there is a URL but it's the wrong kind then clear the old URL and return NoPollingNeeded + if status.PollingURLKind != nil && *status.PollingURLKind != kind { + status.ClearPollingURL() + return PollResultNoPollingNeeded, nil + } + res, err := client.Get(ctx, status.PollingURL) pollErr := errhelp.NewAzureError(err) if pollErr != nil { if pollErr.Type == errhelp.OperationIdNotFound { // Something happened to our OperationId, just clear things out and try again - status.PollingURL = "" + status.ClearPollingURL() } return PollResultTryAgainLater, err } @@ -155,20 +176,26 @@ func (client PollClient) PollLongRunningOperationIfNeeded(ctx context.Context, s if res.Status == LongRunningOperationPollStatusFailed { status.Message = res.Error.Error() // There can be intermediate errors and various other things that cause requests to fail, so we need to try again. - status.PollingURL = "" // Clear URL to force retry + status.ClearPollingURL() + + if res.Error.Code == errhelp.BadRequest { + return PollResultBadRequest, nil + } + return PollResultTryAgainLater, nil } // TODO: May need a notion of fatal error here too - if res.Status == "InProgress" { + if res.Status == "InProgress" || res.Status == "Enqueued" || res.Status == "Dequeued" { // We're waiting for an async op... keep waiting return PollResultTryAgainLater, nil } // Previous operation was a success, clear polling URL and continue if res.Status == LongRunningOperationPollStatusSucceeded { - status.PollingURL = "" + status.ClearPollingURL() + return PollResultCompletedSuccessfully, nil }