From f5044ee38c5b54953b6c25ddf6a24310e3970291 Mon Sep 17 00:00:00 2001 From: sergenyalcin Date: Wed, 3 Nov 2021 22:19:18 +0300 Subject: [PATCH] Add MySQLServerConfiguration managed resource Signed-off-by: sergenyalcin --- apis/database/v1beta1/configuration_types.go | 31 +- apis/database/v1beta1/referencers.go | 35 ++ apis/database/v1beta1/register.go | 9 + .../database/v1beta1/zz_generated.deepcopy.go | 59 +++ apis/database/v1beta1/zz_generated.managed.go | 56 +++ .../v1beta1/zz_generated.managedlist.go | 9 + .../database/mysqlserverconfiguration.yaml | 14 + ...ossplane.io_mysqlserverconfigurations.yaml | 241 ++++++++++ ...ane.io_postgresqlserverconfigurations.yaml | 4 +- pkg/clients/database/configuration/common.go | 22 + .../database/configuration/common_test.go | 40 ++ pkg/clients/database/configuration/mysql.go | 118 +++++ .../database/configuration/mysql_test.go | 81 ++++ .../database/configuration/postgresql.go | 5 - .../database/configuration/postgresql_test.go | 32 +- pkg/controller/azure.go | 2 + .../mysqlserverconfiguration/managed.go | 209 ++++++++ .../mysqlserverconfiguration/managed_test.go | 448 ++++++++++++++++++ .../managed_test.go | 90 ++++ 19 files changed, 1469 insertions(+), 36 deletions(-) create mode 100644 examples/database/mysqlserverconfiguration.yaml create mode 100644 package/crds/database.azure.crossplane.io_mysqlserverconfigurations.yaml create mode 100644 pkg/clients/database/configuration/common.go create mode 100644 pkg/clients/database/configuration/common_test.go create mode 100644 pkg/clients/database/configuration/mysql.go create mode 100644 pkg/clients/database/configuration/mysql_test.go create mode 100644 pkg/controller/database/mysqlserverconfiguration/managed.go create mode 100644 pkg/controller/database/mysqlserverconfiguration/managed_test.go diff --git a/apis/database/v1beta1/configuration_types.go b/apis/database/v1beta1/configuration_types.go index 0526eb08..34387aea 100644 --- a/apis/database/v1beta1/configuration_types.go +++ b/apis/database/v1beta1/configuration_types.go @@ -51,6 +51,33 @@ type PostgreSQLServerConfigurationList struct { Items []PostgreSQLServerConfiguration `json:"items"` } +// +kubebuilder:object:root=true + +// A MySQLServerConfiguration is a managed resource that represents an Azure +// MySQL Server Configuration. +// +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status" +// +kubebuilder:printcolumn:name="VERSION",type="string",JSONPath=".spec.forProvider.version" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster,categories={crossplane,managed,azure} +type MySQLServerConfiguration struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec SQLServerConfigurationSpec `json:"spec"` + Status SQLServerConfigurationStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// MySQLServerConfigurationList contains a list of MySQLServerConfiguration. +type MySQLServerConfigurationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []MySQLServerConfiguration `json:"items"` +} + // SQLServerConfigurationParameters define the desired state of an Azure SQL // Database Server Configuration, either PostgreSQL or MySQL Configuration. type SQLServerConfigurationParameters struct { @@ -74,12 +101,12 @@ type SQLServerConfigurationParameters struct { // +immutable ServerName string `json:"serverName,omitempty"` - // ServerNameRef - A reference to a PostgreSQLServer object to retrieve + // ServerNameRef - A reference to a server object to retrieve // its name // +immutable ServerNameRef *xpv1.Reference `json:"serverNameRef,omitempty"` - // ServerNameSelector - A selector for a PostgreSQLServer object to + // ServerNameSelector - A selector for a server object to // retrieve its name // +immutable ServerNameSelector *xpv1.Selector `json:"serverNameSelector,omitempty"` diff --git a/apis/database/v1beta1/referencers.go b/apis/database/v1beta1/referencers.go index ebc261d2..bfc4f466 100644 --- a/apis/database/v1beta1/referencers.go +++ b/apis/database/v1beta1/referencers.go @@ -48,6 +48,41 @@ func (mg *MySQLServer) ResolveReferences(ctx context.Context, c client.Reader) e return nil } +// ResolveReferences of this MySQLServerConfiguration. +func (mg *MySQLServerConfiguration) ResolveReferences(ctx context.Context, c client.Reader) error { + r := reference.NewAPIResolver(c, mg) + + // Resolve spec.forProvider.resourceGroupName + rsp, err := r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: mg.Spec.ForProvider.ResourceGroupName, + Reference: mg.Spec.ForProvider.ResourceGroupNameRef, + Selector: mg.Spec.ForProvider.ResourceGroupNameSelector, + To: reference.To{Managed: &v1alpha3.ResourceGroup{}, List: &v1alpha3.ResourceGroupList{}}, + Extract: reference.ExternalName(), + }) + if err != nil { + return errors.Wrap(err, "spec.forProvider.resourceGroupName") + } + mg.Spec.ForProvider.ResourceGroupName = rsp.ResolvedValue + mg.Spec.ForProvider.ResourceGroupNameRef = rsp.ResolvedReference + + // Resolve spec.forProvider.resourceGroupName + rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: mg.Spec.ForProvider.ServerName, + Reference: mg.Spec.ForProvider.ServerNameRef, + Selector: mg.Spec.ForProvider.ServerNameSelector, + To: reference.To{Managed: &MySQLServer{}, List: &MySQLServerList{}}, + Extract: reference.ExternalName(), + }) + if err != nil { + return errors.Wrap(err, "spec.forProvider.serverName") + } + mg.Spec.ForProvider.ServerName = rsp.ResolvedValue + mg.Spec.ForProvider.ServerNameRef = rsp.ResolvedReference + + return nil +} + // ResolveReferences of this PostgreSQLServer. func (mg *PostgreSQLServer) ResolveReferences(ctx context.Context, c client.Reader) error { r := reference.NewAPIResolver(c, mg) diff --git a/apis/database/v1beta1/register.go b/apis/database/v1beta1/register.go index 9ff2c848..e74dac6e 100644 --- a/apis/database/v1beta1/register.go +++ b/apis/database/v1beta1/register.go @@ -45,6 +45,14 @@ var ( MySQLServerGroupVersionKind = SchemeGroupVersion.WithKind(MySQLServerKind) ) +// MySQLServerConfiguration type metadata. +var ( + MySQLServerConfigurationKind = reflect.TypeOf(MySQLServerConfiguration{}).Name() + MySQLServerConfigurationGroupKind = schema.GroupKind{Group: Group, Kind: MySQLServerConfigurationKind}.String() + MySQLServerConfigurationKindAPIVersion = MySQLServerConfigurationKind + "." + SchemeGroupVersion.String() + MySQLServerConfigurationGroupVersionKind = SchemeGroupVersion.WithKind(MySQLServerConfigurationKind) +) + // PostgreSQLServer type metadata. var ( PostgreSQLServerKind = reflect.TypeOf(PostgreSQLServer{}).Name() @@ -63,6 +71,7 @@ var ( func init() { SchemeBuilder.Register(&MySQLServer{}, &MySQLServerList{}) + SchemeBuilder.Register(&MySQLServerConfiguration{}, &MySQLServerConfigurationList{}) SchemeBuilder.Register(&PostgreSQLServer{}, &PostgreSQLServerList{}) SchemeBuilder.Register(&PostgreSQLServerConfiguration{}, &PostgreSQLServerConfigurationList{}) } diff --git a/apis/database/v1beta1/zz_generated.deepcopy.go b/apis/database/v1beta1/zz_generated.deepcopy.go index bb3434ec..c6c0e0f6 100644 --- a/apis/database/v1beta1/zz_generated.deepcopy.go +++ b/apis/database/v1beta1/zz_generated.deepcopy.go @@ -52,6 +52,65 @@ func (in *MySQLServer) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MySQLServerConfiguration) DeepCopyInto(out *MySQLServerConfiguration) { + *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 MySQLServerConfiguration. +func (in *MySQLServerConfiguration) DeepCopy() *MySQLServerConfiguration { + if in == nil { + return nil + } + out := new(MySQLServerConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MySQLServerConfiguration) 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 *MySQLServerConfigurationList) DeepCopyInto(out *MySQLServerConfigurationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]MySQLServerConfiguration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MySQLServerConfigurationList. +func (in *MySQLServerConfigurationList) DeepCopy() *MySQLServerConfigurationList { + if in == nil { + return nil + } + out := new(MySQLServerConfigurationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MySQLServerConfigurationList) 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 *MySQLServerList) DeepCopyInto(out *MySQLServerList) { *out = *in diff --git a/apis/database/v1beta1/zz_generated.managed.go b/apis/database/v1beta1/zz_generated.managed.go index 7b873d27..2f83d3e8 100644 --- a/apis/database/v1beta1/zz_generated.managed.go +++ b/apis/database/v1beta1/zz_generated.managed.go @@ -76,6 +76,62 @@ func (mg *MySQLServer) SetWriteConnectionSecretToReference(r *xpv1.SecretReferen mg.Spec.WriteConnectionSecretToReference = r } +// GetCondition of this MySQLServerConfiguration. +func (mg *MySQLServerConfiguration) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this MySQLServerConfiguration. +func (mg *MySQLServerConfiguration) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetProviderConfigReference of this MySQLServerConfiguration. +func (mg *MySQLServerConfiguration) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +/* +GetProviderReference of this MySQLServerConfiguration. +Deprecated: Use GetProviderConfigReference. +*/ +func (mg *MySQLServerConfiguration) GetProviderReference() *xpv1.Reference { + return mg.Spec.ProviderReference +} + +// GetWriteConnectionSecretToReference of this MySQLServerConfiguration. +func (mg *MySQLServerConfiguration) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this MySQLServerConfiguration. +func (mg *MySQLServerConfiguration) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this MySQLServerConfiguration. +func (mg *MySQLServerConfiguration) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetProviderConfigReference of this MySQLServerConfiguration. +func (mg *MySQLServerConfiguration) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +/* +SetProviderReference of this MySQLServerConfiguration. +Deprecated: Use SetProviderConfigReference. +*/ +func (mg *MySQLServerConfiguration) SetProviderReference(r *xpv1.Reference) { + mg.Spec.ProviderReference = r +} + +// SetWriteConnectionSecretToReference of this MySQLServerConfiguration. +func (mg *MySQLServerConfiguration) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} + // GetCondition of this PostgreSQLServer. func (mg *PostgreSQLServer) GetCondition(ct xpv1.ConditionType) xpv1.Condition { return mg.Status.GetCondition(ct) diff --git a/apis/database/v1beta1/zz_generated.managedlist.go b/apis/database/v1beta1/zz_generated.managedlist.go index d4a29c97..15b32aef 100644 --- a/apis/database/v1beta1/zz_generated.managedlist.go +++ b/apis/database/v1beta1/zz_generated.managedlist.go @@ -20,6 +20,15 @@ package v1beta1 import resource "github.com/crossplane/crossplane-runtime/pkg/resource" +// GetItems of this MySQLServerConfigurationList. +func (l *MySQLServerConfigurationList) GetItems() []resource.Managed { + items := make([]resource.Managed, len(l.Items)) + for i := range l.Items { + items[i] = &l.Items[i] + } + return items +} + // GetItems of this MySQLServerList. func (l *MySQLServerList) GetItems() []resource.Managed { items := make([]resource.Managed, len(l.Items)) diff --git a/examples/database/mysqlserverconfiguration.yaml b/examples/database/mysqlserverconfiguration.yaml new file mode 100644 index 00000000..fec12b42 --- /dev/null +++ b/examples/database/mysqlserverconfiguration.yaml @@ -0,0 +1,14 @@ +apiVersion: database.azure.crossplane.io/v1beta1 +kind: MySQLServerConfiguration +metadata: + name: example-mysql-configuration +spec: + providerConfigRef: + name: example + forProvider: + resourceGroupNameRef: + name: example-rg + serverNameRef: + name: example-mysql + name: connect_timeout + value: "15" diff --git a/package/crds/database.azure.crossplane.io_mysqlserverconfigurations.yaml b/package/crds/database.azure.crossplane.io_mysqlserverconfigurations.yaml new file mode 100644 index 00000000..3a9ff9fa --- /dev/null +++ b/package/crds/database.azure.crossplane.io_mysqlserverconfigurations.yaml @@ -0,0 +1,241 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.0 + creationTimestamp: null + name: mysqlserverconfigurations.database.azure.crossplane.io +spec: + group: database.azure.crossplane.io + names: + categories: + - crossplane + - managed + - azure + kind: MySQLServerConfiguration + listKind: MySQLServerConfigurationList + plural: mysqlserverconfigurations + singular: mysqlserverconfiguration + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: READY + type: string + - jsonPath: .status.conditions[?(@.type=='Synced')].status + name: SYNCED + type: string + - jsonPath: .spec.forProvider.version + name: VERSION + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: A MySQLServerConfiguration is a managed resource that represents an Azure MySQL Server Configuration. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: A SQLServerConfigurationSpec defines the desired state of a SQLServer Configuration. + properties: + deletionPolicy: + default: Delete + description: DeletionPolicy specifies what will happen to the underlying external when this managed resource is deleted - either "Delete" or "Orphan" the external resource. + enum: + - Orphan + - Delete + type: string + forProvider: + description: SQLServerConfigurationParameters define the desired state of an Azure SQL Database Server Configuration, either PostgreSQL or MySQL Configuration. + properties: + name: + description: Name - Configuration name to be applied + type: string + resourceGroupName: + description: ResourceGroupName specifies the name of the resource group that should contain this SQLServer. + type: string + resourceGroupNameRef: + description: ResourceGroupNameRef - A reference to a ResourceGroup object to retrieve its name + properties: + name: + description: Name of the referenced object. + type: string + required: + - name + type: object + resourceGroupNameSelector: + description: ResourceGroupNameSelector - A selector for a ResourceGroup object to retrieve its name + properties: + matchControllerRef: + description: MatchControllerRef ensures an object with the same controller reference as the selecting object is selected. + type: boolean + matchLabels: + additionalProperties: + type: string + description: MatchLabels ensures an object with matching labels is selected. + type: object + type: object + serverName: + description: ServerName specifies the name of the server that this configuration applies to. + type: string + serverNameRef: + description: ServerNameRef - A reference to a server object to retrieve its name + properties: + name: + description: Name of the referenced object. + type: string + required: + - name + type: object + serverNameSelector: + description: ServerNameSelector - A selector for a server object to retrieve its name + properties: + matchControllerRef: + description: MatchControllerRef ensures an object with the same controller reference as the selecting object is selected. + type: boolean + matchLabels: + additionalProperties: + type: string + description: MatchLabels ensures an object with matching labels is selected. + type: object + type: object + value: + description: Value - Configuration value to be applied Can be left unset to read the current value as a result of late-initialization. + type: string + required: + - name + type: object + providerConfigRef: + default: + name: default + description: ProviderConfigReference specifies how the provider that will be used to create, observe, update, and delete this managed resource should be configured. + properties: + name: + description: Name of the referenced object. + type: string + required: + - name + type: object + providerRef: + description: 'ProviderReference specifies the provider that will be used to create, observe, update, and delete this managed resource. Deprecated: Please use ProviderConfigReference, i.e. `providerConfigRef`' + properties: + name: + description: Name of the referenced object. + type: string + required: + - name + type: object + writeConnectionSecretToRef: + description: WriteConnectionSecretToReference specifies the namespace and name of a Secret to which any connection details for this managed resource should be written. Connection details frequently include the endpoint, username, and password required to connect to the managed resource. + properties: + name: + description: Name of the secret. + type: string + namespace: + description: Namespace of the secret. + type: string + required: + - name + - namespace + type: object + required: + - forProvider + type: object + status: + description: A SQLServerConfigurationStatus represents the observed state of a SQLServerConfiguration. + properties: + atProvider: + description: SQLServerConfigurationObservation represents the current state of Azure SQL resource. + properties: + dataType: + description: DataType - Data type for the configuration + type: string + defaultValue: + description: DefaultValue - Default value for this configuration + type: string + description: + description: Description - Description for the configuration + type: string + id: + description: ID - Resource ID + type: string + lastOperation: + description: LastOperation represents the state of the last operation started by the controller. + properties: + errorMessage: + description: ErrorMessage represents the error that occurred during the operation. + type: string + method: + description: Method is HTTP method that the initial request is made with. + type: string + pollingUrl: + description: PollingURL is used to fetch the status of the given operation. + type: string + status: + description: Status represents the status of the operation. + type: string + type: object + name: + description: Name - Resource name. + type: string + source: + description: Source - Applied configuration source + type: string + type: + description: Type - Resource type. + type: string + value: + description: Value - Applied configuration value + type: string + type: object + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastTransitionTime: + description: LastTransitionTime is the last time this condition transitioned from one status to another. + format: date-time + type: string + message: + description: A Message containing details about this condition's last transition from one status to another, if any. + type: string + reason: + description: A Reason for this condition's last transition from one status to another. + type: string + status: + description: Status of this condition; is it currently True, False, or Unknown? + type: string + type: + description: Type of this condition. At most one of each condition type may apply to a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/package/crds/database.azure.crossplane.io_postgresqlserverconfigurations.yaml b/package/crds/database.azure.crossplane.io_postgresqlserverconfigurations.yaml index e874da46..8ed6b3c7 100644 --- a/package/crds/database.azure.crossplane.io_postgresqlserverconfigurations.yaml +++ b/package/crds/database.azure.crossplane.io_postgresqlserverconfigurations.yaml @@ -88,7 +88,7 @@ spec: description: ServerName specifies the name of the server that this configuration applies to. type: string serverNameRef: - description: ServerNameRef - A reference to a PostgreSQLServer object to retrieve its name + description: ServerNameRef - A reference to a server object to retrieve its name properties: name: description: Name of the referenced object. @@ -97,7 +97,7 @@ spec: - name type: object serverNameSelector: - description: ServerNameSelector - A selector for a PostgreSQLServer object to retrieve its name + description: ServerNameSelector - A selector for a server object to retrieve its name properties: matchControllerRef: description: MatchControllerRef ensures an object with the same controller reference as the selecting object is selected. diff --git a/pkg/clients/database/configuration/common.go b/pkg/clients/database/configuration/common.go new file mode 100644 index 00000000..8784ef54 --- /dev/null +++ b/pkg/clients/database/configuration/common.go @@ -0,0 +1,22 @@ +/* +Copyright 2021 The Crossplane 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 configuration + +const ( + // SourceSystemManaged represents the source for system-managed configuration values + SourceSystemManaged = "system-default" +) diff --git a/pkg/clients/database/configuration/common_test.go b/pkg/clients/database/configuration/common_test.go new file mode 100644 index 00000000..d15afb33 --- /dev/null +++ b/pkg/clients/database/configuration/common_test.go @@ -0,0 +1,40 @@ +/* +Copyright 2021 The Crossplane 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 configuration + +import azuredbv1beta1 "github.com/crossplane/provider-azure/apis/database/v1beta1" + +const ( + testValue1 = "testValue1" + testValue2 = "testValue2" +) + +type sqlServerConfigurationParametersModifier func(*azuredbv1beta1.SQLServerConfigurationParameters) + +func sqlServerConfigurationParametersWithValue(v *string) sqlServerConfigurationParametersModifier { + return func(cm *azuredbv1beta1.SQLServerConfigurationParameters) { + cm.Value = v + } +} + +func sqlServerConfigurationParameters(sm ...sqlServerConfigurationParametersModifier) *azuredbv1beta1.SQLServerConfigurationParameters { + cm := &azuredbv1beta1.SQLServerConfigurationParameters{} + for _, m := range sm { + m(cm) + } + return cm +} diff --git a/pkg/clients/database/configuration/mysql.go b/pkg/clients/database/configuration/mysql.go new file mode 100644 index 00000000..7c959302 --- /dev/null +++ b/pkg/clients/database/configuration/mysql.go @@ -0,0 +1,118 @@ +/* +Copyright 2021 The Crossplane 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 configuration + +import ( + "context" + "net/http" + + "github.com/Azure/azure-sdk-for-go/services/mysql/mgmt/2017-12-01/mysql" + "github.com/Azure/go-autorest/autorest" + + azuredbv1beta1 "github.com/crossplane/provider-azure/apis/database/v1beta1" + "github.com/crossplane/provider-azure/apis/v1alpha3" + azure "github.com/crossplane/provider-azure/pkg/clients" +) + +// NOTE: postgresql and mysql structs and functions live in their respective +// packages even though they are exactly the same. However, Crossplane does not +// make that assumption and use the respective package for each type, although, +// they both share the same SQLServerParameters and SQLServerObservation objects. +// https://github.com/Azure/azure-sdk-for-go/blob/master/services/mysql/mgmt/2017-12-01/mysql/models.go +// https://github.com/Azure/azure-sdk-for-go/blob/master/services/postgresql/mgmt/2017-12-01/postgresql/models.go + +// MySQLConfigurationAPI represents the API interface for a MySQL Server Configuration client +type MySQLConfigurationAPI interface { + Get(ctx context.Context, s *azuredbv1beta1.MySQLServerConfiguration) (mysql.Configuration, error) + CreateOrUpdate(ctx context.Context, s *azuredbv1beta1.MySQLServerConfiguration) error + Delete(ctx context.Context, s *azuredbv1beta1.MySQLServerConfiguration) error + GetRESTClient() autorest.Sender +} + +// MySQLConfigurationClient is the concreate implementation of the MySQLConfigurationAPI interface for MySQL that calls Azure API. +type MySQLConfigurationClient struct { + mysql.ConfigurationsClient +} + +// NewMySQLConfigurationClient creates and initializes a MySQLConfigurationClient instance. +func NewMySQLConfigurationClient(cl mysql.ConfigurationsClient) *MySQLConfigurationClient { + return &MySQLConfigurationClient{ + ConfigurationsClient: cl, + } +} + +// GetRESTClient returns the underlying REST client that the client object uses. +func (c *MySQLConfigurationClient) GetRESTClient() autorest.Sender { + return c.ConfigurationsClient.Client +} + +// Get retrieves the requested MySQL Configuration +func (c *MySQLConfigurationClient) Get(ctx context.Context, cr *azuredbv1beta1.MySQLServerConfiguration) (mysql.Configuration, error) { + return c.ConfigurationsClient.Get(ctx, cr.Spec.ForProvider.ResourceGroupName, cr.Spec.ForProvider.ServerName, cr.Spec.ForProvider.Name) +} + +// CreateOrUpdate creates or updates a MySQL Server Configuration +func (c *MySQLConfigurationClient) CreateOrUpdate(ctx context.Context, cr *azuredbv1beta1.MySQLServerConfiguration) error { + return c.update(ctx, cr, cr.Spec.ForProvider.Value, nil) +} + +// Delete deletes the given MySQL Server Configuration +func (c *MySQLConfigurationClient) Delete(ctx context.Context, cr *azuredbv1beta1.MySQLServerConfiguration) error { + source := SourceSystemManaged + // we are mimicking Terraform behavior here: when the configuration object + // is deleted, we are resetting its value to the system default, + // and updating its source to "system-default" to declare that + // we are no longer managing it. + return c.update(ctx, cr, &cr.Status.AtProvider.DefaultValue, &source) +} + +func (c *MySQLConfigurationClient) update(ctx context.Context, cr *azuredbv1beta1.MySQLServerConfiguration, value, source *string) error { + s := cr.Spec.ForProvider + config := mysql.Configuration{ + ConfigurationProperties: &mysql.ConfigurationProperties{ + Value: value, + Source: source, + }, + } + op, err := c.ConfigurationsClient.CreateOrUpdate(ctx, s.ResourceGroupName, cr.Spec.ForProvider.ServerName, cr.Spec.ForProvider.Name, config) + if err != nil { + return err + } + cr.Status.AtProvider.LastOperation = v1alpha3.AsyncOperation{ + PollingURL: op.PollingURL(), + Method: http.MethodPut, + } + return nil +} + +// UpdateMySQLConfigurationObservation produces SQLServerConfigurationObservation from mysql.Configuration. +func UpdateMySQLConfigurationObservation(o *azuredbv1beta1.SQLServerConfigurationObservation, in mysql.Configuration) { + o.ID = azure.ToString(in.ID) + o.Name = azure.ToString(in.Name) + o.Type = azure.ToString(in.Type) + o.DataType = azure.ToString(in.DataType) + o.Value = azure.ToString(in.Value) + o.DefaultValue = azure.ToString(in.DefaultValue) + o.Source = azure.ToString(in.Source) + o.Description = azure.ToString(in.Description) +} + +// IsMySQLConfigurationUpToDate is used to report whether given mysql.Configuration is in +// sync with the SQLServerConfigurationParameters that user desires. +func IsMySQLConfigurationUpToDate(p azuredbv1beta1.SQLServerConfigurationParameters, in mysql.Configuration) bool { + return azure.ToString(p.Value) == azure.ToString(in.Value) +} diff --git a/pkg/clients/database/configuration/mysql_test.go b/pkg/clients/database/configuration/mysql_test.go new file mode 100644 index 00000000..01126fc8 --- /dev/null +++ b/pkg/clients/database/configuration/mysql_test.go @@ -0,0 +1,81 @@ +/* +Copyright 2021 The Crossplane 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 configuration + +import ( + "testing" + + "github.com/Azure/azure-sdk-for-go/services/mysql/mgmt/2017-12-01/mysql" + "github.com/google/go-cmp/cmp" + + azuredbv1beta1 "github.com/crossplane/provider-azure/apis/database/v1beta1" +) + +type mysqlConfigurationModifier func(configuration *mysql.Configuration) + +func mysqlConfigurationWithValue(v *string) mysqlConfigurationModifier { + return func(configuration *mysql.Configuration) { + configuration.Value = v + } +} + +func mysqlConfiguration(cm ...mysqlConfigurationModifier) *mysql.Configuration { + c := &mysql.Configuration{ + ConfigurationProperties: &mysql.ConfigurationProperties{}, + } + for _, m := range cm { + m(c) + } + return c +} + +func TestIsMySQLConfigurationUpToDate(t *testing.T) { + val1, val2 := testValue1, testValue2 + type args struct { + p azuredbv1beta1.SQLServerConfigurationParameters + in mysql.Configuration + } + tests := map[string]struct { + args args + want bool + }{ + "UpToDate": { + args: args{ + p: *sqlServerConfigurationParameters( + sqlServerConfigurationParametersWithValue(&val1)), + in: *mysqlConfiguration(mysqlConfigurationWithValue(&val1)), + }, + want: true, + }, + "NeedsUpdate": { + args: args{ + p: *sqlServerConfigurationParameters( + sqlServerConfigurationParametersWithValue(&val1)), + in: *mysqlConfiguration(mysqlConfigurationWithValue(&val2)), + }, + want: false, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + got := IsMySQLConfigurationUpToDate(tt.args.p, tt.args.in) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("IsMySQLConfigurationUpToDate(...): -want, +got\n%s", diff) + } + }) + } +} diff --git a/pkg/clients/database/configuration/postgresql.go b/pkg/clients/database/configuration/postgresql.go index 7c3b1d06..44053f84 100644 --- a/pkg/clients/database/configuration/postgresql.go +++ b/pkg/clients/database/configuration/postgresql.go @@ -28,11 +28,6 @@ import ( azure "github.com/crossplane/provider-azure/pkg/clients" ) -const ( - // SourceSystemManaged represents the source for system-managed configuration values - SourceSystemManaged = "system-default" -) - // NOTE: postgresql and mysql structs and functions live in their respective // packages even though they are exactly the same. However, Crossplane does not // make that assumption and use the respective package for each type, although, diff --git a/pkg/clients/database/configuration/postgresql_test.go b/pkg/clients/database/configuration/postgresql_test.go index 0b17a8d1..b9e59a00 100644 --- a/pkg/clients/database/configuration/postgresql_test.go +++ b/pkg/clients/database/configuration/postgresql_test.go @@ -23,39 +23,17 @@ import ( "github.com/google/go-cmp/cmp" "github.com/crossplane/provider-azure/apis/database/v1beta1" - azuredbv1beta1 "github.com/crossplane/provider-azure/apis/database/v1beta1" ) -const ( - testValue1 = "testValue1" - testValue2 = "testValue2" -) - -type sqlServerConfigurationParametersModifier func(*azuredbv1beta1.SQLServerConfigurationParameters) - -func sqlServerConfigurationParametersWithValue(v *string) sqlServerConfigurationParametersModifier { - return func(cm *azuredbv1beta1.SQLServerConfigurationParameters) { - cm.Value = v - } -} - -func sqlServerConfigurationParameters(sm ...sqlServerConfigurationParametersModifier) *azuredbv1beta1.SQLServerConfigurationParameters { - cm := &azuredbv1beta1.SQLServerConfigurationParameters{} - for _, m := range sm { - m(cm) - } - return cm -} - -type configurationModifier func(configuration *postgresql.Configuration) +type postgresqlConfigurationModifier func(configuration *postgresql.Configuration) -func configurationWithValue(v *string) configurationModifier { +func postgresqlConfigurationWithValue(v *string) postgresqlConfigurationModifier { return func(configuration *postgresql.Configuration) { configuration.Value = v } } -func configuration(cm ...configurationModifier) *postgresql.Configuration { +func postgresqlConfiguration(cm ...postgresqlConfigurationModifier) *postgresql.Configuration { c := &postgresql.Configuration{ ConfigurationProperties: &postgresql.ConfigurationProperties{}, } @@ -79,7 +57,7 @@ func TestIsPostgreSQLConfigurationUpToDate(t *testing.T) { args: args{ p: *sqlServerConfigurationParameters( sqlServerConfigurationParametersWithValue(&val1)), - in: *configuration(configurationWithValue(&val1)), + in: *postgresqlConfiguration(postgresqlConfigurationWithValue(&val1)), }, want: true, }, @@ -87,7 +65,7 @@ func TestIsPostgreSQLConfigurationUpToDate(t *testing.T) { args: args{ p: *sqlServerConfigurationParameters( sqlServerConfigurationParametersWithValue(&val1)), - in: *configuration(configurationWithValue(&val2)), + in: *postgresqlConfiguration(postgresqlConfigurationWithValue(&val2)), }, want: false, }, diff --git a/pkg/controller/azure.go b/pkg/controller/azure.go index 34db8246..ff4ce82f 100644 --- a/pkg/controller/azure.go +++ b/pkg/controller/azure.go @@ -29,6 +29,7 @@ import ( "github.com/crossplane/provider-azure/pkg/controller/config" "github.com/crossplane/provider-azure/pkg/controller/database/cosmosdb" "github.com/crossplane/provider-azure/pkg/controller/database/mysqlserver" + "github.com/crossplane/provider-azure/pkg/controller/database/mysqlserverconfiguration" "github.com/crossplane/provider-azure/pkg/controller/database/mysqlserverfirewallrule" "github.com/crossplane/provider-azure/pkg/controller/database/mysqlservervirtualnetworkrule" "github.com/crossplane/provider-azure/pkg/controller/database/postgresqlserver" @@ -51,6 +52,7 @@ func Setup(mgr ctrl.Manager, l logging.Logger, rl workqueue.RateLimiter, poll ti mysqlserver.Setup, mysqlserverfirewallrule.Setup, mysqlservervirtualnetworkrule.Setup, + mysqlserverconfiguration.Setup, postgresqlserver.Setup, postgresqlserverfirewallrule.Setup, postgresqlservervirtualnetworkrule.Setup, diff --git a/pkg/controller/database/mysqlserverconfiguration/managed.go b/pkg/controller/database/mysqlserverconfiguration/managed.go new file mode 100644 index 00000000..e89cc0e0 --- /dev/null +++ b/pkg/controller/database/mysqlserverconfiguration/managed.go @@ -0,0 +1,209 @@ +/* +Copyright 2021 The Crossplane 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 mysqlserverconfiguration + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/azure-sdk-for-go/services/mysql/mgmt/2017-12-01/mysql" + "github.com/pkg/errors" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/pkg/meta" + "github.com/crossplane/crossplane-runtime/pkg/ratelimiter" + "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/pkg/resource" + + "github.com/crossplane/provider-azure/apis/database/v1beta1" + azure "github.com/crossplane/provider-azure/pkg/clients" + "github.com/crossplane/provider-azure/pkg/clients/database/configuration" +) + +const ( + // error messages + errNotMySQLServerConfig = "managed resource is not a MySQLServerConfiguration" + errCreateMySQLServerConfig = "cannot create MySQLServerConfiguration" + errUpdateMySQLServerConfig = "cannot update MySQLServerConfiguration" + errGetMySQLServerConfig = "cannot get MySQLServerConfiguration" + errDeleteMySQLServerConfig = "cannot delete MySQLServerConfiguration" + errFetchLastOperation = "cannot fetch last operation" + errNotFoundMySQLServerConfig = "the specified MySQLServerConfiguration does not exist" + + fmtExternalName = "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.DBforMySQL/servers/%s/configurations/%s" +) + +// Setup adds a controller that reconciles MySQLInstances. +func Setup(mgr ctrl.Manager, l logging.Logger, rl workqueue.RateLimiter, poll time.Duration) error { + name := managed.ControllerName(v1beta1.MySQLServerConfigurationGroupKind) + + return ctrl.NewControllerManagedBy(mgr). + Named(name). + WithOptions(controller.Options{ + RateLimiter: ratelimiter.NewDefaultManagedRateLimiter(rl), + }). + For(&v1beta1.MySQLServerConfiguration{}). + Complete(managed.NewReconciler(mgr, + resource.ManagedKind(v1beta1.MySQLServerConfigurationGroupVersionKind), + managed.WithExternalConnecter(&connecter{client: mgr.GetClient()}), + managed.WithReferenceResolver(managed.NewAPISimpleReferenceResolver(mgr.GetClient())), + managed.WithInitializers(managed.NewDefaultProviderConfig(mgr.GetClient())), + managed.WithPollInterval(poll), + managed.WithLogger(l.WithValues("controller", name)), + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))))) +} + +type connecter struct { + client client.Client +} + +func (c *connecter) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { + creds, auth, err := azure.GetAuthInfo(ctx, c.client, mg) + if err != nil { + return nil, err + } + cl := mysql.NewConfigurationsClient(creds[azure.CredentialsKeySubscriptionID]) + cl.Authorizer = auth + return &external{ + kube: c.client, + client: configuration.NewMySQLConfigurationClient(cl), + subscriptionID: creds[azure.CredentialsKeySubscriptionID], + }, nil +} + +type external struct { + kube client.Client + client configuration.MySQLConfigurationAPI + subscriptionID string +} + +func (e external) generateExtName(resourceGroupName, serverName, configName string) string { + return fmt.Sprintf(fmtExternalName, e.subscriptionID, resourceGroupName, serverName, configName) +} + +func (e *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { // nolint:gocyclo + // cyclomatic complexity of this method (13) is slightly higher than our goal of 10. + cr, ok := mg.(*v1beta1.MySQLServerConfiguration) + if !ok { + return managed.ExternalObservation{}, errors.New(errNotMySQLServerConfig) + } + config, err := e.client.Get(ctx, cr) + if azure.IsNotFound(err) { + // Valid configurations are pre-determined in server side and new ones cannot be created. + // Only existing valid configurations can be updated. + // Therefore, if the config cannot be found in the result of the get call, instead of returning nil, an error + // is returned and the error is reported in status conditions of the MySQLServerConfiguration managed resource. + return managed.ExternalObservation{}, errors.Wrap(err, errNotFoundMySQLServerConfig) + } + if err != nil { + return managed.ExternalObservation{}, errors.Wrap(err, errGetMySQLServerConfig) + } + // ARM does not return a 404 for the configuration resource even if we set its value to the server default + // and source to "system-default". Hence, we check those conditions here: + if meta.WasDeleted(cr) && cr.Status.AtProvider.Source == configuration.SourceSystemManaged && cr.Status.AtProvider.Value == cr.Status.AtProvider.DefaultValue { + return managed.ExternalObservation{ + ResourceExists: false, + }, nil + } + // it's possible that external.Create has never been called, thus set ext. name if not set + if meta.GetExternalName(cr) == "" { + meta.SetExternalName(cr, e.generateExtName(cr.Spec.ForProvider.ResourceGroupName, + cr.Spec.ForProvider.ServerName, cr.Spec.ForProvider.Name)) + } + + l := resource.NewLateInitializer() + cr.Spec.ForProvider.Value = l.LateInitializeStringPtr(cr.Spec.ForProvider.Value, config.Value) + + configuration.UpdateMySQLConfigurationObservation(&cr.Status.AtProvider, config) + // We make this call after kube.Update since it doesn't update the + // status subresource but fetches the whole object after it's done. So, + // changes to status has to be done after kube.Update in order not to get them + // lost. + if err := azure.FetchAsyncOperation(ctx, e.client.GetRESTClient(), &cr.Status.AtProvider.LastOperation); err != nil { + return managed.ExternalObservation{}, errors.Wrap(err, errFetchLastOperation) + } + // if the configuration has been applied successfully, then mark MR as available + if cr.Status.AtProvider.Value == azure.ToString(cr.Spec.ForProvider.Value) { + cr.SetConditions(xpv1.Available()) + } else { + cr.SetConditions(xpv1.Unavailable()) + } + + return managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: configuration.IsMySQLConfigurationUpToDate(cr.Spec.ForProvider, config), + ResourceLateInitialized: l.IsChanged(), + }, nil +} + +func (e *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { + cr, ok := mg.(*v1beta1.MySQLServerConfiguration) + if !ok { + return managed.ExternalCreation{}, errors.New(errNotMySQLServerConfig) + } + + if err := e.client.CreateOrUpdate(ctx, cr); err != nil { + return managed.ExternalCreation{}, errors.Wrap(err, errCreateMySQLServerConfig) + } + // no error if ext name does not match + meta.SetExternalName(cr, e.generateExtName(cr.Spec.ForProvider.ResourceGroupName, + cr.Spec.ForProvider.ServerName, cr.Spec.ForProvider.Name)) + + return managed.ExternalCreation{ + ExternalNameAssigned: true, + }, errors.Wrap( + azure.FetchAsyncOperation(ctx, e.client.GetRESTClient(), &cr.Status.AtProvider.LastOperation), + errFetchLastOperation) +} + +func (e *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { + cr, ok := mg.(*v1beta1.MySQLServerConfiguration) + if !ok { + return managed.ExternalUpdate{}, errors.New(errNotMySQLServerConfig) + } + if cr.Status.AtProvider.LastOperation.Status == azure.AsyncOperationStatusInProgress { + return managed.ExternalUpdate{}, nil + } + if err := e.client.CreateOrUpdate(ctx, cr); err != nil { + return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateMySQLServerConfig) + } + + return managed.ExternalUpdate{}, errors.Wrap( + azure.FetchAsyncOperation(ctx, e.client.GetRESTClient(), &cr.Status.AtProvider.LastOperation), + errFetchLastOperation) +} + +func (e *external) Delete(ctx context.Context, mg resource.Managed) error { + cr, ok := mg.(*v1beta1.MySQLServerConfiguration) + if !ok { + return errors.New(errNotMySQLServerConfig) + } + + if err := e.client.Delete(ctx, cr); resource.Ignore(azure.IsNotFound, err) != nil { + return errors.Wrap(err, errDeleteMySQLServerConfig) + } + return errors.Wrap( + azure.FetchAsyncOperation(ctx, e.client.GetRESTClient(), &cr.Status.AtProvider.LastOperation), + errFetchLastOperation) +} diff --git a/pkg/controller/database/mysqlserverconfiguration/managed_test.go b/pkg/controller/database/mysqlserverconfiguration/managed_test.go new file mode 100644 index 00000000..3916c126 --- /dev/null +++ b/pkg/controller/database/mysqlserverconfiguration/managed_test.go @@ -0,0 +1,448 @@ +/* +Copyright 2021 The Crossplane 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 mysqlserverconfiguration + +import ( + "context" + "net/http" + "testing" + + "github.com/Azure/azure-sdk-for-go/services/mysql/mgmt/2017-12-01/mysql" + "github.com/Azure/go-autorest/autorest" + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" + + "github.com/crossplane/crossplane-runtime/pkg/meta" + "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/pkg/resource" + "github.com/crossplane/crossplane-runtime/pkg/test" + + "github.com/crossplane/provider-azure/apis/database/v1beta1" + azurev1alpha3 "github.com/crossplane/provider-azure/apis/v1alpha3" +) + +const ( + inProgress = "InProgress" + subscriptID = "subscription-id" +) + +type MockMySQLConfigurationAPI struct { + MockGet func(ctx context.Context, s *v1beta1.MySQLServerConfiguration) (mysql.Configuration, error) + MockCreateOrUpdate func(ctx context.Context, s *v1beta1.MySQLServerConfiguration) error + MockDelete func(ctx context.Context, s *v1beta1.MySQLServerConfiguration) error + MockGetRESTClient func() autorest.Sender +} + +func (m *MockMySQLConfigurationAPI) Get(ctx context.Context, s *v1beta1.MySQLServerConfiguration) (mysql.Configuration, error) { + return m.MockGet(ctx, s) +} + +func (m *MockMySQLConfigurationAPI) CreateOrUpdate(ctx context.Context, s *v1beta1.MySQLServerConfiguration) error { + return m.MockCreateOrUpdate(ctx, s) +} + +func (m *MockMySQLConfigurationAPI) Delete(ctx context.Context, s *v1beta1.MySQLServerConfiguration) error { + return m.MockDelete(ctx, s) +} + +func (m *MockMySQLConfigurationAPI) GetRESTClient() autorest.Sender { + return m.MockGetRESTClient() +} + +type modifier func(configuration *v1beta1.MySQLServerConfiguration) + +func withLastOperation(op azurev1alpha3.AsyncOperation) modifier { + return func(p *v1beta1.MySQLServerConfiguration) { + p.Status.AtProvider.LastOperation = op + } +} + +func withExternalName(name string) modifier { + return func(p *v1beta1.MySQLServerConfiguration) { + meta.SetExternalName(p, name) + } +} + +func mysqlserverconfiguration(m ...modifier) *v1beta1.MySQLServerConfiguration { + p := &v1beta1.MySQLServerConfiguration{} + + for _, mod := range m { + mod(p) + } + return p +} + +func TestObserve(t *testing.T) { + errBoom := errors.New("boom") + name := "coolserver" + + type args struct { + ctx context.Context + mg resource.Managed + } + type want struct { + eo managed.ExternalObservation + err error + } + + cases := map[string]struct { + e managed.ExternalClient + args args + want want + }{ + "ErrNotAMySQLServerConfiguration": { + e: &external{}, + args: args{ + ctx: context.Background(), + }, + want: want{ + err: errors.New(errNotMySQLServerConfig), + }, + }, + "ErrGetServer": { + e: &external{ + client: &MockMySQLConfigurationAPI{ + MockGet: func(_ context.Context, _ *v1beta1.MySQLServerConfiguration) (mysql.Configuration, error) { + return mysql.Configuration{}, errBoom + }, + }, + }, + args: args{ + ctx: context.Background(), + mg: mysqlserverconfiguration(), + }, + want: want{ + err: errors.Wrap(errBoom, errGetMySQLServerConfig), + }, + }, + "ServerCreating": { + e: &external{ + client: &MockMySQLConfigurationAPI{ + MockGet: func(_ context.Context, _ *v1beta1.MySQLServerConfiguration) (mysql.Configuration, error) { + return mysql.Configuration{}, autorest.DetailedError{StatusCode: http.StatusNotFound} + }, + }, + }, + args: args{ + ctx: context.Background(), + mg: mysqlserverconfiguration(withLastOperation(azurev1alpha3.AsyncOperation{Method: http.MethodPut, PollingURL: "crossplane.io"})), + }, + want: want{ + eo: managed.ExternalObservation{ + ResourceExists: false, + }, + err: errors.Wrap(autorest.DetailedError{StatusCode: http.StatusNotFound}, errNotFoundMySQLServerConfig), + }, + }, + "ServerNotFound": { + e: &external{ + client: &MockMySQLConfigurationAPI{ + MockGet: func(_ context.Context, _ *v1beta1.MySQLServerConfiguration) (mysql.Configuration, error) { + return mysql.Configuration{}, autorest.DetailedError{StatusCode: http.StatusNotFound} + }, + MockGetRESTClient: func() autorest.Sender { + return nil + }, + }, + }, + args: args{ + ctx: context.Background(), + mg: mysqlserverconfiguration(), + }, + want: want{ + eo: managed.ExternalObservation{ + ResourceExists: false, + }, + err: errors.Wrap(autorest.DetailedError{StatusCode: http.StatusNotFound}, errNotFoundMySQLServerConfig), + }, + }, + "ServerAvailable": { + e: &external{ + kube: &test.MockClient{ + MockUpdate: test.NewMockUpdateFn(nil), + }, + client: &MockMySQLConfigurationAPI{ + MockGet: func(_ context.Context, _ *v1beta1.MySQLServerConfiguration) (mysql.Configuration, error) { + return mysql.Configuration{ + ConfigurationProperties: &mysql.ConfigurationProperties{}, + }, nil + }, + MockGetRESTClient: func() autorest.Sender { + return autorest.SenderFunc(func(*http.Request) (*http.Response, error) { + return nil, nil + }) + }, + }, + }, + args: args{ + ctx: context.Background(), + mg: mysqlserverconfiguration( + withExternalName(name), + ), + }, + want: want{ + eo: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + eo, err := tc.e.Observe(tc.args.ctx, tc.args.mg) + + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("tc.e.Observe(...): -want error, +got error:\n%s", diff) + } + if diff := cmp.Diff(tc.want.eo, eo); diff != "" { + t.Errorf("tc.e.Observe(...): -want, +got:\n%s", diff) + } + }) + } +} + +func TestCreate(t *testing.T) { + errBoom := errors.New("boom") + + type args struct { + ctx context.Context + mg resource.Managed + } + type want struct { + ec managed.ExternalCreation + err error + } + + cases := map[string]struct { + e managed.ExternalClient + args args + want want + }{ + "ErrNotAMySQLServerConfiguration": { + e: &external{}, + args: args{ + ctx: context.Background(), + }, + want: want{ + err: errors.New(errNotMySQLServerConfig), + }, + }, + "ErrCreateServer": { + e: &external{ + client: &MockMySQLConfigurationAPI{ + MockCreateOrUpdate: func(_ context.Context, _ *v1beta1.MySQLServerConfiguration) error { return errBoom }, + }, + subscriptionID: subscriptID, + }, + args: args{ + ctx: context.Background(), + mg: mysqlserverconfiguration(), + }, + want: want{ + err: errors.Wrap(errBoom, errCreateMySQLServerConfig), + }, + }, + "Successful": { + e: &external{ + client: &MockMySQLConfigurationAPI{ + MockCreateOrUpdate: func(_ context.Context, _ *v1beta1.MySQLServerConfiguration) error { return nil }, + MockGetRESTClient: func() autorest.Sender { + return autorest.SenderFunc(func(*http.Request) (*http.Response, error) { + return nil, nil + }) + }, + }, + subscriptionID: subscriptID, + }, + args: args{ + ctx: context.Background(), + mg: mysqlserverconfiguration(), + }, + want: want{ + ec: managed.ExternalCreation{ + ExternalNameAssigned: true, + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + ec, err := tc.e.Create(tc.args.ctx, tc.args.mg) + + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("tc.e.Create(...): -want error, +got error:\n%s", diff) + } + if diff := cmp.Diff(tc.want.ec, ec); diff != "" { + t.Errorf("tc.e.Create(...): -want, +got:\n%s", diff) + } + }) + } +} + +func TestDelete(t *testing.T) { + errBoom := errors.New("boom") + + type args struct { + ctx context.Context + mg resource.Managed + } + + cases := map[string]struct { + e managed.ExternalClient + args args + want error + }{ + "ErrNotAMySQLServerConfiguration": { + e: &external{}, + args: args{ + ctx: context.Background(), + }, + want: errors.New(errNotMySQLServerConfig), + }, + "ErrDeleteServer": { + e: &external{ + client: &MockMySQLConfigurationAPI{ + MockDelete: func(_ context.Context, _ *v1beta1.MySQLServerConfiguration) error { return errBoom }, + }, + }, + args: args{ + ctx: context.Background(), + mg: mysqlserverconfiguration(), + }, + want: errors.Wrap(errBoom, errDeleteMySQLServerConfig), + }, + "Successful": { + e: &external{ + client: &MockMySQLConfigurationAPI{ + MockDelete: func(_ context.Context, _ *v1beta1.MySQLServerConfiguration) error { return nil }, + MockGetRESTClient: func() autorest.Sender { + return autorest.SenderFunc(func(*http.Request) (*http.Response, error) { + return nil, nil + }) + }, + }, + }, + args: args{ + ctx: context.Background(), + mg: mysqlserverconfiguration(), + }, + want: nil, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := tc.e.Delete(tc.args.ctx, tc.args.mg) + + if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { + t.Errorf("tc.e.Delete(...): -want error, +got error:\n%s", diff) + } + }) + } +} + +func TestUpdate(t *testing.T) { + errBoom := errors.New("boom") + + type args struct { + ctx context.Context + mg resource.Managed + } + + type want struct { + eu managed.ExternalUpdate + err error + } + + cases := map[string]struct { + e managed.ExternalClient + args args + want want + }{ + "ErrNotAMySQLServerConfiguration": { + e: &external{}, + args: args{ + ctx: context.Background(), + }, + want: want{ + err: errors.New(errNotMySQLServerConfig), + }, + }, + "ServerUpdating": { + e: &external{}, + args: args{ + ctx: context.Background(), + mg: mysqlserverconfiguration(withLastOperation(azurev1alpha3.AsyncOperation{ + Method: http.MethodPatch, PollingURL: "crossplane.io", Status: inProgress})), + }, + want: want{ + err: nil, + }, + }, + "ErrUpdateServer": { + e: &external{ + client: &MockMySQLConfigurationAPI{ + MockCreateOrUpdate: func(_ context.Context, _ *v1beta1.MySQLServerConfiguration) error { return errBoom }, + }, + subscriptionID: subscriptID, + }, + args: args{ + ctx: context.Background(), + mg: mysqlserverconfiguration(), + }, + want: want{ + err: errors.Wrap(errBoom, errUpdateMySQLServerConfig), + }, + }, + "Successful": { + e: &external{ + client: &MockMySQLConfigurationAPI{ + MockCreateOrUpdate: func(_ context.Context, _ *v1beta1.MySQLServerConfiguration) error { return nil }, + MockGetRESTClient: func() autorest.Sender { + return autorest.SenderFunc(func(*http.Request) (*http.Response, error) { + return nil, nil + }) + }, + }, + subscriptionID: subscriptID, + }, + args: args{ + ctx: context.Background(), + mg: mysqlserverconfiguration(), + }, + want: want{ + err: nil, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + eu, err := tc.e.Update(tc.args.ctx, tc.args.mg) + + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("tc.e.Update(...): -want error, +got error:\n%s", diff) + } + if diff := cmp.Diff(tc.want.eu, eu); diff != "" { + t.Errorf("tc.e.Update(...): -want, +got:\n%s", diff) + } + }) + } +} diff --git a/pkg/controller/database/postgresqlserverconfiguration/managed_test.go b/pkg/controller/database/postgresqlserverconfiguration/managed_test.go index 537e34ae..fc1a19cb 100644 --- a/pkg/controller/database/postgresqlserverconfiguration/managed_test.go +++ b/pkg/controller/database/postgresqlserverconfiguration/managed_test.go @@ -37,6 +37,7 @@ import ( ) const ( + inProgress = "InProgress" inProgressResponse = `{"status": "InProgress"}` subscriptID = "subscription-id" ) @@ -366,3 +367,92 @@ func TestDelete(t *testing.T) { }) } } + +func TestUpdate(t *testing.T) { + errBoom := errors.New("boom") + + type args struct { + ctx context.Context + mg resource.Managed + } + + type want struct { + eu managed.ExternalUpdate + err error + } + + cases := map[string]struct { + e managed.ExternalClient + args args + want want + }{ + "ErrNotAPostgreSQLServerConfiguration": { + e: &external{}, + args: args{ + ctx: context.Background(), + }, + want: want{ + err: errors.New(errNotPostgreSQLServerConfig), + }, + }, + "ServerUpdating": { + e: &external{}, + args: args{ + ctx: context.Background(), + mg: postgresqlserverconfiguration(withLastOperation(azurev1alpha3.AsyncOperation{ + Method: http.MethodPatch, PollingURL: "crossplane.io", Status: inProgress})), + }, + want: want{ + err: nil, + }, + }, + "ErrUpdateServer": { + e: &external{ + client: &MockPostgreSQLConfigurationAPI{ + MockCreateOrUpdate: func(_ context.Context, _ *v1beta1.PostgreSQLServerConfiguration) error { return errBoom }, + }, + subscriptionID: subscriptID, + }, + args: args{ + ctx: context.Background(), + mg: postgresqlserverconfiguration(), + }, + want: want{ + err: errors.Wrap(errBoom, errUpdatePostgreSQLServerConfig), + }, + }, + "Successful": { + e: &external{ + client: &MockPostgreSQLConfigurationAPI{ + MockCreateOrUpdate: func(_ context.Context, _ *v1beta1.PostgreSQLServerConfiguration) error { return nil }, + MockGetRESTClient: func() autorest.Sender { + return autorest.SenderFunc(func(*http.Request) (*http.Response, error) { + return nil, nil + }) + }, + }, + subscriptionID: subscriptID, + }, + args: args{ + ctx: context.Background(), + mg: postgresqlserverconfiguration(), + }, + want: want{ + err: nil, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + eu, err := tc.e.Update(tc.args.ctx, tc.args.mg) + + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("tc.e.Update(...): -want error, +got error:\n%s", diff) + } + if diff := cmp.Diff(tc.want.eu, eu); diff != "" { + t.Errorf("tc.e.Update(...): -want, +got:\n%s", diff) + } + }) + } +}