Repository: crossplane-contrib/provider-sql
Pull request #138 (2023-03-15 16:11:11)

Fix mysql provider config spec.tls kubebuilder validation enum
<!--
Thank you for helping to improve Crossplane!

Please read through https://git.io/fj2m9 if this is your first time opening a
Crossplane pull request. Find us in https://slack.crossplane.io/messages/dev if
you need any help contributing.
-->

### Description of your changes

<!--
Briefly describe what this pull request does. Be sure to direct your reviewers'
attention to anything that needs special consideration.

We love pull requests that resolve an open Crossplane issue. If yours does, you
can uncomment the below line to indicate which issue your PR fixes, for example
"Fixes #500":

-->
Fixes [#137](https://github.com/crossplane-contrib/provider-sql/issues/137)

I have:

- [x] Read and followed Crossplane's [contribution process].
- [x] Run `make reviewable` to ensure this PR is ready for review.

### How has this code been tested
Since the current unit tests do not setup a webhook server it kinda hard to add an automatic test for this.
Tested manually with kubectl and a local kind k8s cluster
```yaml
apiVersion: mysql.sql.crossplane.io/v1alpha1
kind: ProviderConfig
metadata:
  name: default
spec:
  credentials:
    source: MySQLConnectionSecret
    connectionSecretRef:
      namespace: default
      name: db-conn
  # tls one of preferred(default), skip-verify, or true
  tls: true
```
```bash
> kubectl apply -f /home/alejandro/Documents/operators/crossplane/provider-sql/examples/mysql/config.yaml
The ProviderConfig "default2" is invalid: 
* spec.tls: Invalid value: "boolean": spec.tls in body must be of type string: "boolean"
* spec.tls: Unsupported value: true: supported values: "true", "skip-verify", "preferred"
```
```yaml
apiVersion: mysql.sql.crossplane.io/v1alpha1
kind: ProviderConfig
metadata:
  name: default
spec:
  credentials:
    source: MySQLConnectionSecret
    connectionSecretRef:
      namespace: default
      name: db-conn
  # tls one of preferred(default), skip-verify, or true
  tls: "true"
```
```bash
kubectl apply -f /home/alejandro/Documents/operators/crossplane/provider-sql/examples/mysql/config.yaml
providerconfig.mysql.sql.crossplane.io/default2 created
```
<!--
Before reviewers can be confident in the correctness of this pull request, it
needs to tested and shown to be correct. Briefly describe the testing that has
already been done or which is planned for this change.
-->

[contribution process]: https://git.io/fj2m9



Patch:
diff --git a/apis/mysql/v1alpha1/grant_types.go b/apis/mysql/v1alpha1/grant_types.go
index d15c6e4..2667736 100644
--- a/apis/mysql/v1alpha1/grant_types.go
+++ b/apis/mysql/v1alpha1/grant_types.go
@@ -97,6 +97,13 @@ type GrantParameters struct {
 // A GrantStatus represents the observed state of a Grant.
 type GrantStatus struct {
 	xpv1.ResourceStatus `json:",inline"`
+	AtProvider          GrantObservation `json:"atProvider,omitempty"`
+}
+
+// A GrantObservation represents the observed state of a MySQL grant.
+type GrantObservation struct {
+	// Privileges represents the applied privileges
+	Privileges []string `json:"privileges,omitempty"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/apis/mysql/v1alpha1/provider_types.go b/apis/mysql/v1alpha1/provider_types.go
index f38f687..b193f65 100644
--- a/apis/mysql/v1alpha1/provider_types.go
+++ b/apis/mysql/v1alpha1/provider_types.go
@@ -31,7 +31,7 @@ type ProviderConfigSpec struct {
 	// or use preferred to use TLS only when advertised by the server. This is similar
 	// to skip-verify, but additionally allows a fallback to a connection which is
 	// not encrypted. Neither skip-verify nor preferred add any reliable security.
-	// +kubebuilder:validation:Enum=true;skip-verify;preferred
+	// +kubebuilder:validation:Enum="true";skip-verify;preferred
 	// +optional
 	TLS *string `json:"tls"`
 }
diff --git a/apis/mysql/v1alpha1/zz_generated.deepcopy.go b/apis/mysql/v1alpha1/zz_generated.deepcopy.go
index 1ef6357..6a8ab1d 100644
--- a/apis/mysql/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/mysql/v1alpha1/zz_generated.deepcopy.go
@@ -176,6 +176,26 @@ func (in *GrantList) DeepCopyObject() runtime.Object {
 	return nil
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GrantObservation) DeepCopyInto(out *GrantObservation) {
+	*out = *in
+	if in.Privileges != nil {
+		in, out := &in.Privileges, &out.Privileges
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrantObservation.
+func (in *GrantObservation) DeepCopy() *GrantObservation {
+	if in == nil {
+		return nil
+	}
+	out := new(GrantObservation)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *GrantParameters) DeepCopyInto(out *GrantParameters) {
 	*out = *in
@@ -271,6 +291,7 @@ func (in *GrantSpec) DeepCopy() *GrantSpec {
 func (in *GrantStatus) DeepCopyInto(out *GrantStatus) {
 	*out = *in
 	in.ResourceStatus.DeepCopyInto(&out.ResourceStatus)
+	in.AtProvider.DeepCopyInto(&out.AtProvider)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrantStatus.
diff --git a/cmd/provider/main.go b/cmd/provider/main.go
index fa43aff..fa8bc72 100644
--- a/cmd/provider/main.go
+++ b/cmd/provider/main.go
@@ -28,6 +28,7 @@ import (
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 
+	xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller"
 	"github.com/crossplane/crossplane-runtime/pkg/logging"
 
 	"github.com/crossplane-contrib/provider-sql/apis"
@@ -38,8 +39,9 @@ func main() {
 	var (
 		app            = kingpin.New(filepath.Base(os.Args[0]), "SQL support for Crossplane.").DefaultEnvars()
 		debug          = app.Flag("debug", "Run with debug logging.").Short('d').Bool()
+		pollInterval   = app.Flag("poll", "Poll interval controls how often an individual resource should be checked for drift.").Default("10m").Duration()
 		syncPeriod     = app.Flag("sync", "Controller manager sync period such as 300ms, 1.5h, or 2h45m").Short('s').Default("1h").Duration()
-		leaderElection = app.Flag("leader-election", "Use leader election for the conroller manager.").Short('l').Default("false").OverrideDefaultFromEnvar("LEADER_ELECTION").Bool()
+		leaderElection = app.Flag("leader-election", "Use leader election for the conroller manager.").Short('l').Default("false").Envar("LEADER_ELECTION").Bool()
 	)
 	kingpin.MustParse(app.Parse(os.Args[1:]))
 
@@ -63,8 +65,13 @@ func main() {
 		SyncPeriod:       syncPeriod,
 	})
 	kingpin.FatalIfError(err, "Cannot create controller manager")
-
 	kingpin.FatalIfError(apis.AddToScheme(mgr.GetScheme()), "Cannot add SQL APIs to scheme")
-	kingpin.FatalIfError(controller.Setup(mgr, log), "Cannot setup SQL controllers")
+
+	o := xpcontroller.Options{
+		Logger:       log,
+		PollInterval: *pollInterval,
+	}
+
+	kingpin.FatalIfError(controller.Setup(mgr, o), "Cannot setup SQL controllers")
 	kingpin.FatalIfError(mgr.Start(ctrl.SetupSignalHandler()), "Cannot start controller manager")
 }
diff --git a/examples/mysql/grant_all_databases.yaml b/examples/mysql/grant_all_databases.yaml
new file mode 100644
index 0000000..0ffad8c
--- /dev/null
+++ b/examples/mysql/grant_all_databases.yaml
@@ -0,0 +1,12 @@
+apiVersion: mysql.sql.crossplane.io/v1alpha1
+kind: Grant
+metadata:
+  name: example-grant-all-databases
+spec:
+  forProvider:
+    privileges:
+      - DROP
+      - CREATE ROUTINE
+      - EVENT
+    userRef:
+      name: example-user
diff --git a/examples/mysql/grant.yaml b/examples/mysql/grant_database.yaml
similarity index 88%
rename from examples/mysql/grant.yaml
rename to examples/mysql/grant_database.yaml
index 37c89b6..3c80bab 100644
--- a/examples/mysql/grant.yaml
+++ b/examples/mysql/grant_database.yaml
@@ -1,7 +1,7 @@
 apiVersion: mysql.sql.crossplane.io/v1alpha1
 kind: Grant
 metadata:
-  name: example-grant
+  name: example-grant-database
 spec:
   forProvider:
     privileges:
diff --git a/package/crds/mysql.sql.crossplane.io_grants.yaml b/package/crds/mysql.sql.crossplane.io_grants.yaml
index 1e3fabc..850fcde 100644
--- a/package/crds/mysql.sql.crossplane.io_grants.yaml
+++ b/package/crds/mysql.sql.crossplane.io_grants.yaml
@@ -416,6 +416,16 @@ spec:
           status:
             description: A GrantStatus represents the observed state of a Grant.
             properties:
+              atProvider:
+                description: A GrantObservation represents the observed state of a
+                  MySQL grant.
+                properties:
+                  privileges:
+                    description: Privileges represents the applied privileges
+                    items:
+                      type: string
+                    type: array
+                type: object
               conditions:
                 description: Conditions of the resource.
                 items:
diff --git a/package/crds/mysql.sql.crossplane.io_providerconfigs.yaml b/package/crds/mysql.sql.crossplane.io_providerconfigs.yaml
index a01c018..0a8c39a 100644
--- a/package/crds/mysql.sql.crossplane.io_providerconfigs.yaml
+++ b/package/crds/mysql.sql.crossplane.io_providerconfigs.yaml
@@ -81,7 +81,7 @@ spec:
                   allows a fallback to a connection which is not encrypted. Neither
                   skip-verify nor preferred add any reliable security.
                 enum:
-                - true
+                - "true"
                 - skip-verify
                 - preferred
                 type: string
diff --git a/pkg/controller/mssql/config/config.go b/pkg/controller/mssql/config/config.go
index 1aaa6ad..4efa359 100644
--- a/pkg/controller/mssql/config/config.go
+++ b/pkg/controller/mssql/config/config.go
@@ -20,8 +20,8 @@ import (
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/source"
 
+	"github.com/crossplane/crossplane-runtime/pkg/controller"
 	"github.com/crossplane/crossplane-runtime/pkg/event"
-	"github.com/crossplane/crossplane-runtime/pkg/logging"
 	"github.com/crossplane/crossplane-runtime/pkg/reconciler/providerconfig"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 
@@ -30,7 +30,7 @@ import (
 
 // Setup adds a controller that reconciles ProviderConfigs by accounting for
 // their current usage.
-func Setup(mgr ctrl.Manager, l logging.Logger) error {
+func Setup(mgr ctrl.Manager, o controller.Options) error {
 	name := providerconfig.ControllerName(v1alpha1.ProviderConfigGroupKind)
 
 	of := resource.ProviderConfigKinds{
@@ -43,6 +43,6 @@ func Setup(mgr ctrl.Manager, l logging.Logger) error {
 		For(&v1alpha1.ProviderConfig{}).
 		Watches(&source.Kind{Type: &v1alpha1.ProviderConfigUsage{}}, &resource.EnqueueRequestForProviderConfig{}).
 		Complete(providerconfig.NewReconciler(mgr, of,
-			providerconfig.WithLogger(l.WithValues("controller", name)),
+			providerconfig.WithLogger(o.Logger.WithValues("controller", name)),
 			providerconfig.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))))
 }
diff --git a/pkg/controller/mssql/database/reconciler.go b/pkg/controller/mssql/database/reconciler.go
index 89bce53..f1a2537 100644
--- a/pkg/controller/mssql/database/reconciler.go
+++ b/pkg/controller/mssql/database/reconciler.go
@@ -18,7 +18,6 @@ package database
 
 import (
 	"context"
-	"time"
 
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
@@ -28,8 +27,8 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller"
 	"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/reconciler/managed"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
@@ -54,15 +53,15 @@ const (
 )
 
 // Setup adds a controller that reconciles Database managed resources.
-func Setup(mgr ctrl.Manager, l logging.Logger) error {
+func Setup(mgr ctrl.Manager, o xpcontroller.Options) error {
 	name := managed.ControllerName(v1alpha1.DatabaseGroupKind)
 
 	t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{})
 	r := managed.NewReconciler(mgr,
 		resource.ManagedKind(v1alpha1.DatabaseGroupVersionKind),
 		managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newClient: mssql.New}),
-		managed.WithLogger(l.WithValues("controller", name)),
-		managed.WithPollInterval(10*time.Minute),
+		managed.WithLogger(o.Logger.WithValues("controller", name)),
+		managed.WithPollInterval(o.PollInterval),
 		managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))))
 
 	return ctrl.NewControllerManagedBy(mgr).
diff --git a/pkg/controller/mssql/grant/reconciler.go b/pkg/controller/mssql/grant/reconciler.go
index 40f9be7..ee2077a 100644
--- a/pkg/controller/mssql/grant/reconciler.go
+++ b/pkg/controller/mssql/grant/reconciler.go
@@ -21,7 +21,6 @@ import (
 	"fmt"
 	"sort"
 	"strings"
-	"time"
 
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
@@ -32,8 +31,8 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller"
 	"github.com/crossplane/crossplane-runtime/pkg/event"
-	"github.com/crossplane/crossplane-runtime/pkg/logging"
 	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 
@@ -57,7 +56,7 @@ const (
 )
 
 // Setup adds a controller that reconciles Grant managed resources.
-func Setup(mgr ctrl.Manager, l logging.Logger) error {
+func Setup(mgr ctrl.Manager, o xpcontroller.Options) error {
 	name := managed.ControllerName(v1alpha1.GrantGroupKind)
 
 	t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{})
@@ -65,8 +64,8 @@ func Setup(mgr ctrl.Manager, l logging.Logger) error {
 		resource.ManagedKind(v1alpha1.GrantGroupVersionKind),
 		managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newClient: mssql.New}),
 		managed.WithReferenceResolver(managed.NewAPISimpleReferenceResolver(mgr.GetClient())),
-		managed.WithLogger(l.WithValues("controller", name)),
-		managed.WithPollInterval(10*time.Minute),
+		managed.WithLogger(o.Logger.WithValues("controller", name)),
+		managed.WithPollInterval(o.PollInterval),
 		managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))))
 
 	return ctrl.NewControllerManagedBy(mgr).
diff --git a/pkg/controller/mssql/mssql.go b/pkg/controller/mssql/mssql.go
index 5e00bc8..d583d72 100644
--- a/pkg/controller/mssql/mssql.go
+++ b/pkg/controller/mssql/mssql.go
@@ -17,26 +17,26 @@ limitations under the License.
 package mssql
 
 import (
-	"github.com/crossplane-contrib/provider-sql/pkg/controller/mssql/grant"
 	ctrl "sigs.k8s.io/controller-runtime"
 
-	"github.com/crossplane/crossplane-runtime/pkg/logging"
+	"github.com/crossplane/crossplane-runtime/pkg/controller"
 
 	"github.com/crossplane-contrib/provider-sql/pkg/controller/mssql/config"
 	"github.com/crossplane-contrib/provider-sql/pkg/controller/mssql/database"
+	"github.com/crossplane-contrib/provider-sql/pkg/controller/mssql/grant"
 	"github.com/crossplane-contrib/provider-sql/pkg/controller/mssql/user"
 )
 
 // Setup creates all MSSQL controllers with the supplied logger and adds
 // them to the supplied manager.
-func Setup(mgr ctrl.Manager, l logging.Logger) error {
-	for _, setup := range []func(ctrl.Manager, logging.Logger) error{
+func Setup(mgr ctrl.Manager, o controller.Options) error {
+	for _, setup := range []func(ctrl.Manager, controller.Options) error{
 		config.Setup,
 		database.Setup,
 		user.Setup,
 		grant.Setup,
 	} {
-		if err := setup(mgr, l); err != nil {
+		if err := setup(mgr, o); err != nil {
 			return err
 		}
 	}
diff --git a/pkg/controller/mssql/user/reconciler.go b/pkg/controller/mssql/user/reconciler.go
index fb1bc1e..b6f66cd 100644
--- a/pkg/controller/mssql/user/reconciler.go
+++ b/pkg/controller/mssql/user/reconciler.go
@@ -19,7 +19,6 @@ package user
 import (
 	"context"
 	"fmt"
-	"time"
 
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
@@ -30,8 +29,8 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller"
 	"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/password"
 	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
@@ -59,15 +58,15 @@ const (
 )
 
 // Setup adds a controller that reconciles User managed resources.
-func Setup(mgr ctrl.Manager, l logging.Logger) error {
+func Setup(mgr ctrl.Manager, o xpcontroller.Options) error {
 	name := managed.ControllerName(v1alpha1.UserGroupKind)
 
 	t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{})
 	r := managed.NewReconciler(mgr,
 		resource.ManagedKind(v1alpha1.UserGroupVersionKind),
 		managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newClient: mssql.New}),
-		managed.WithLogger(l.WithValues("controller", name)),
-		managed.WithPollInterval(10*time.Minute),
+		managed.WithLogger(o.Logger.WithValues("controller", name)),
+		managed.WithPollInterval(o.PollInterval),
 		managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))))
 
 	return ctrl.NewControllerManagedBy(mgr).
diff --git a/pkg/controller/mysql/config/config.go b/pkg/controller/mysql/config/config.go
index 100d98d..893d1a6 100644
--- a/pkg/controller/mysql/config/config.go
+++ b/pkg/controller/mysql/config/config.go
@@ -20,8 +20,8 @@ import (
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/source"
 
+	"github.com/crossplane/crossplane-runtime/pkg/controller"
 	"github.com/crossplane/crossplane-runtime/pkg/event"
-	"github.com/crossplane/crossplane-runtime/pkg/logging"
 	"github.com/crossplane/crossplane-runtime/pkg/reconciler/providerconfig"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 
@@ -30,7 +30,7 @@ import (
 
 // Setup adds a controller that reconciles ProviderConfigs by accounting for
 // their current usage.
-func Setup(mgr ctrl.Manager, l logging.Logger) error {
+func Setup(mgr ctrl.Manager, o controller.Options) error {
 	name := providerconfig.ControllerName(v1alpha1.ProviderConfigGroupKind)
 
 	of := resource.ProviderConfigKinds{
@@ -43,6 +43,6 @@ func Setup(mgr ctrl.Manager, l logging.Logger) error {
 		For(&v1alpha1.ProviderConfig{}).
 		Watches(&source.Kind{Type: &v1alpha1.ProviderConfigUsage{}}, &resource.EnqueueRequestForProviderConfig{}).
 		Complete(providerconfig.NewReconciler(mgr, of,
-			providerconfig.WithLogger(l.WithValues("controller", name)),
+			providerconfig.WithLogger(o.Logger.WithValues("controller", name)),
 			providerconfig.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))))
 }
diff --git a/pkg/controller/mysql/database/reconciler.go b/pkg/controller/mysql/database/reconciler.go
index 4892376..00bbe62 100644
--- a/pkg/controller/mysql/database/reconciler.go
+++ b/pkg/controller/mysql/database/reconciler.go
@@ -18,7 +18,6 @@ package database
 
 import (
 	"context"
-	"time"
 
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
@@ -28,8 +27,8 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller"
 	"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/reconciler/managed"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
@@ -54,15 +53,15 @@ const (
 )
 
 // Setup adds a controller that reconciles Database managed resources.
-func Setup(mgr ctrl.Manager, l logging.Logger) error {
+func Setup(mgr ctrl.Manager, o xpcontroller.Options) error {
 	name := managed.ControllerName(v1alpha1.DatabaseGroupKind)
 
 	t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{})
 	r := managed.NewReconciler(mgr,
 		resource.ManagedKind(v1alpha1.DatabaseGroupVersionKind),
 		managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: mysql.New}),
-		managed.WithLogger(l.WithValues("controller", name)),
-		managed.WithPollInterval(10*time.Minute),
+		managed.WithLogger(o.Logger.WithValues("controller", name)),
+		managed.WithPollInterval(o.PollInterval),
 		managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))))
 
 	return ctrl.NewControllerManagedBy(mgr).
diff --git a/pkg/controller/mysql/grant/reconciler.go b/pkg/controller/mysql/grant/reconciler.go
index 1861e66..286cae6 100644
--- a/pkg/controller/mysql/grant/reconciler.go
+++ b/pkg/controller/mysql/grant/reconciler.go
@@ -22,7 +22,6 @@ import (
 	"regexp"
 	"sort"
 	"strings"
-	"time"
 
 	mysqldriver "github.com/go-sql-driver/mysql"
 	"github.com/pkg/errors"
@@ -33,8 +32,8 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller"
 	"github.com/crossplane/crossplane-runtime/pkg/event"
-	"github.com/crossplane/crossplane-runtime/pkg/logging"
 	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 
@@ -65,7 +64,7 @@ var (
 )
 
 // Setup adds a controller that reconciles Grant managed resources.
-func Setup(mgr ctrl.Manager, l logging.Logger) error {
+func Setup(mgr ctrl.Manager, o xpcontroller.Options) error {
 	name := managed.ControllerName(v1alpha1.GrantGroupKind)
 
 	t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{})
@@ -73,8 +72,8 @@ func Setup(mgr ctrl.Manager, l logging.Logger) error {
 		resource.ManagedKind(v1alpha1.GrantGroupVersionKind),
 		managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: mysql.New}),
 		managed.WithReferenceResolver(managed.NewAPISimpleReferenceResolver(mgr.GetClient())),
-		managed.WithLogger(l.WithValues("controller", name)),
-		managed.WithPollInterval(10*time.Minute),
+		managed.WithLogger(o.Logger.WithValues("controller", name)),
+		managed.WithPollInterval(o.PollInterval),
 		managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))))
 
 	return ctrl.NewControllerManagedBy(mgr).
@@ -143,7 +142,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex
 	dbname := defaultIdentifier(cr.Spec.ForProvider.Database)
 	table := defaultIdentifier(cr.Spec.ForProvider.Table)
 
-	privileges, result, err := c.getPrivileges(ctx, username, dbname, table)
+	observedPrivileges, result, err := c.getPrivileges(ctx, username, dbname, table)
 	if err != nil {
 		return managed.ExternalObservation{}, err
 	}
@@ -151,18 +150,16 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex
 		return *result, nil
 	}
 
-	if !privilegesEqual(cr.Spec.ForProvider.Privileges.ToStringSlice(), privileges) {
-		return managed.ExternalObservation{
-			ResourceExists:   true,
-			ResourceUpToDate: false,
-		}, nil
-	}
+	cr.Status.AtProvider.Privileges = observedPrivileges
+
+	desiredPrivileges := cr.Spec.ForProvider.Privileges.ToStringSlice()
+	toGrant, toRevoke := diffPermissions(desiredPrivileges, observedPrivileges)
 
 	cr.SetConditions(xpv1.Available())
 
 	return managed.ExternalObservation{
 		ResourceExists:   true,
-		ResourceUpToDate: true,
+		ResourceUpToDate: len(toGrant) == 0 && len(toRevoke) == 0,
 	}, nil
 }
 
@@ -174,24 +171,6 @@ func defaultIdentifier(identifier *string) string {
 	return "*"
 }
 
-func privilegesEqual(a, b []string) bool {
-	if len(a) != len(b) {
-		return false
-	}
-	sort.Strings(a)
-	sort.Strings(b)
-
-	for i := range a {
-		// Special case because ALL is an alias for "ALL PRIVILEGES"
-		strA := strings.ReplaceAll(a[i], allPrivileges, "ALL")
-		strB := strings.ReplaceAll(b[i], allPrivileges, "ALL")
-		if strA != strB {
-			return false
-		}
-	}
-	return true
-}
-
 func parseGrant(grant, dbname string, table string) (privileges []string) {
 	matches := grantRegex.FindStringSubmatch(grant)
 	if len(matches) == 4 && matches[2] == dbname && matches[3] == table {
@@ -202,10 +181,42 @@ func parseGrant(grant, dbname string, table string) (privileges []string) {
 
 func (c *external) getPrivileges(ctx context.Context, username, dbname string, table string) ([]string, *managed.ExternalObservation, error) {
 	username, host := mysql.SplitUserHost(username)
+
+	privileges, err := c.parseGrantRows(ctx, username, host, dbname, table)
+	if err != nil {
+		var myErr *mysqldriver.MySQLError
+		if errors.As(err, &myErr) && myErr.Number == errCodeNoSuchGrant {
+			// The user doesn't (yet) exist and therefore no grants either
+			return nil, &managed.ExternalObservation{ResourceExists: false}, nil
+		}
+
+		return nil, nil, errors.Wrap(err, errCurrentGrant)
+	}
+
+	// In mysql when all grants are revoked from user, it still grants usage (meaning no
+	// privileges) on *.* So the grant can be considered as non existent, just like when
+	// privileges slice is nil/empty
+	// https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.html#priv_usage
+	var ret []string
+	for _, p := range privileges {
+		if p != "USAGE" {
+			ret = append(ret, p)
+		}
+	}
+
+	if ret == nil {
+		return nil, &managed.ExternalObservation{ResourceExists: false}, nil
+	}
+
+	return ret, nil, nil
+}
+
+func (c *external) parseGrantRows(ctx context.Context, username string, host string, dbname string, table string) ([]string, error) {
 	query := fmt.Sprintf("SHOW GRANTS FOR %s@%s", mysql.QuoteValue(username), mysql.QuoteValue(host))
 	rows, err := c.db.Query(ctx, xsql.Query{String: query})
+
 	if err != nil {
-		return nil, nil, errors.Wrap(err, errCurrentGrant)
+		return nil, err
 	}
 	defer rows.Close() //nolint:errcheck
 
@@ -213,7 +224,7 @@ func (c *external) getPrivileges(ctx context.Context, username, dbname string, t
 	for rows.Next() {
 		var grant string
 		if err := rows.Scan(&grant); err != nil {
-			return nil, nil, errors.Wrap(err, errCurrentGrant)
+			return nil, err
 		}
 		p := parseGrant(grant, dbname, table)
 
@@ -225,17 +236,10 @@ func (c *external) getPrivileges(ctx context.Context, username, dbname string, t
 	}
 
 	if err := rows.Err(); err != nil {
-		var myErr *mysqldriver.MySQLError
-		if errors.As(err, &myErr) && myErr.Number == errCodeNoSuchGrant {
-			// The user doesn't (yet) exist and therefore no grants either
-			return nil, &managed.ExternalObservation{ResourceExists: false}, nil
-		}
-		return nil, nil, errors.Wrap(err, errCurrentGrant)
-	}
-	if privileges == nil {
-		return nil, &managed.ExternalObservation{ResourceExists: false}, nil
+		return nil, err
 	}
-	return privileges, nil, nil
+
+	return privileges, nil
 }
 
 func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) {
@@ -268,30 +272,50 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext
 	dbname := defaultIdentifier(cr.Spec.ForProvider.Database)
 	table := defaultIdentifier(cr.Spec.ForProvider.Table)
 
-	privileges := strings.Join(cr.Spec.ForProvider.Privileges.ToStringSlice(), ", ")
 	username, host := mysql.SplitUserHost(username)
 
-	// Remove current grants since it's not possible to update grants.
-	// This might leave applications with no access to the DB for a short time
-	// until the privileges are granted again.
-	// Using a transaction is unfortunately not possible because a GRANT triggers
-	// an implicit commit: https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html
-	query := fmt.Sprintf("REVOKE ALL ON %s.%s FROM %s@%s",
-		dbname,
-		table,
-		mysql.QuoteValue(username),
-		mysql.QuoteValue(host),
-	)
-	if err := c.db.Exec(ctx, xsql.Query{String: query}); err != nil {
-		return managed.ExternalUpdate{}, errors.Wrap(err, errRevokeGrant)
+	observed := cr.Status.AtProvider.Privileges
+	desired := cr.Spec.ForProvider.Privileges.ToStringSlice()
+	toGrant, toRevoke := diffPermissions(desired, observed)
+
+	if len(toRevoke) > 0 {
+		sort.Strings(toRevoke)
+		query := fmt.Sprintf("REVOKE %s ON %s.%s FROM %s@%s",
+			strings.Join(toRevoke, ", "),
+			dbname,
+			table,
+			mysql.QuoteValue(username),
+			mysql.QuoteValue(host),
+		)
+
+		if err := c.db.Exec(ctx, xsql.Query{String: query}); err != nil {
+			return managed.ExternalUpdate{}, errors.Wrap(err, errRevokeGrant)
+		}
+
+		if err := c.db.Exec(ctx, xsql.Query{String: "FLUSH PRIVILEGES"}); err != nil {
+			return managed.ExternalUpdate{}, errors.Wrap(err, errFlushPriv)
+		}
 	}
+	if len(toGrant) > 0 {
+		sort.Strings(toGrant)
+		query := fmt.Sprintf("GRANT %s ON %s.%s TO %s@%s",
+			strings.Join(toGrant, ", "),
+			dbname,
+			table,
+			mysql.QuoteValue(username),
+			mysql.QuoteValue(host),
+		)
+
+		if err := c.db.Exec(ctx, xsql.Query{String: query}); err != nil {
+			return managed.ExternalUpdate{}, errors.Wrap(err, errCreateGrant)
+		}
 
-	query = createGrantQuery(privileges, dbname, username, table)
-	if err := c.db.Exec(ctx, xsql.Query{String: query}); err != nil {
-		return managed.ExternalUpdate{}, err
+		if err := c.db.Exec(ctx, xsql.Query{String: "FLUSH PRIVILEGES"}); err != nil {
+			return managed.ExternalUpdate{}, errors.Wrap(err, errFlushPriv)
+		}
 	}
-	err := c.db.Exec(ctx, xsql.Query{String: "FLUSH PRIVILEGES"})
-	return managed.ExternalUpdate{}, errors.Wrap(err, errFlushPriv)
+
+	return managed.ExternalUpdate{}, nil
 }
 
 func createGrantQuery(privileges, dbname, username string, table string) string {
@@ -339,3 +363,36 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error {
 	err := c.db.Exec(ctx, xsql.Query{String: "FLUSH PRIVILEGES"})
 	return errors.Wrap(err, errFlushPriv)
 }
+
+func diffPermissions(desired, observed []string) ([]string, []string) {
+	desiredMap := make(map[string]struct{}, len(desired))
+	observedMap := make(map[string]struct{}, len(observed))
+
+	for _, desiredPrivilege := range desired {
+		// Special case because ALL is an alias for "ALL PRIVILEGES"
+		desiredPrivilegeMapped := strings.ReplaceAll(desiredPrivilege, allPrivileges, "ALL")
+		desiredMap[desiredPrivilegeMapped] = struct{}{}
+	}
+	for _, observedPrivilege := range observed {
+		// Special case because ALL is an alias for "ALL PRIVILEGES"
+		observedPrivilegeMapped := strings.ReplaceAll(observedPrivilege, allPrivileges, "ALL")
+		observedMap[observedPrivilegeMapped] = struct{}{}
+	}
+
+	var toGrant []string
+	var toRevoke []string
+
+	for desiredPrivilege := range desiredMap {
+		if _, ok := observedMap[desiredPrivilege]; !ok {
+			toGrant = append(toGrant, desiredPrivilege)
+		}
+	}
+
+	for observedPrivilege := range observedMap {
+		if _, ok := desiredMap[observedPrivilege]; !ok {
+			toRevoke = append(toRevoke, observedPrivilege)
+		}
+	}
+
+	return toGrant, toRevoke
+}
diff --git a/pkg/controller/mysql/mysql.go b/pkg/controller/mysql/mysql.go
index 3c4f09b..abc8e22 100644
--- a/pkg/controller/mysql/mysql.go
+++ b/pkg/controller/mysql/mysql.go
@@ -19,7 +19,7 @@ package mysql
 import (
 	ctrl "sigs.k8s.io/controller-runtime"
 
-	"github.com/crossplane/crossplane-runtime/pkg/logging"
+	"github.com/crossplane/crossplane-runtime/pkg/controller"
 
 	"github.com/crossplane-contrib/provider-sql/pkg/controller/mysql/config"
 	"github.com/crossplane-contrib/provider-sql/pkg/controller/mysql/database"
@@ -29,14 +29,14 @@ import (
 
 // Setup creates all MySQL controllers with the supplied logger and adds
 // them to the supplied manager.
-func Setup(mgr ctrl.Manager, l logging.Logger) error {
-	for _, setup := range []func(ctrl.Manager, logging.Logger) error{
+func Setup(mgr ctrl.Manager, o controller.Options) error {
+	for _, setup := range []func(ctrl.Manager, controller.Options) error{
 		config.Setup,
 		database.Setup,
 		user.Setup,
 		grant.Setup,
 	} {
-		if err := setup(mgr, l); err != nil {
+		if err := setup(mgr, o); err != nil {
 			return err
 		}
 	}
diff --git a/pkg/controller/mysql/user/reconciler.go b/pkg/controller/mysql/user/reconciler.go
index 8b3fff2..78f4496 100644
--- a/pkg/controller/mysql/user/reconciler.go
+++ b/pkg/controller/mysql/user/reconciler.go
@@ -20,7 +20,6 @@ import (
 	"context"
 	"fmt"
 	"strings"
-	"time"
 
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
@@ -30,8 +29,8 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller"
 	"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/password"
 	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
@@ -61,15 +60,15 @@ const (
 )
 
 // Setup adds a controller that reconciles User managed resources.
-func Setup(mgr ctrl.Manager, l logging.Logger) error {
+func Setup(mgr ctrl.Manager, o xpcontroller.Options) error {
 	name := managed.ControllerName(v1alpha1.UserGroupKind)
 
 	t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{})
 	r := managed.NewReconciler(mgr,
 		resource.ManagedKind(v1alpha1.UserGroupVersionKind),
 		managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: mysql.New}),
-		managed.WithLogger(l.WithValues("controller", name)),
-		managed.WithPollInterval(10*time.Minute),
+		managed.WithLogger(o.Logger.WithValues("controller", name)),
+		managed.WithPollInterval(o.PollInterval),
 		managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))))
 
 	return ctrl.NewControllerManagedBy(mgr).
diff --git a/pkg/controller/postgresql/config/config.go b/pkg/controller/postgresql/config/config.go
index 8fff1e7..6594539 100644
--- a/pkg/controller/postgresql/config/config.go
+++ b/pkg/controller/postgresql/config/config.go
@@ -20,8 +20,8 @@ import (
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/source"
 
+	"github.com/crossplane/crossplane-runtime/pkg/controller"
 	"github.com/crossplane/crossplane-runtime/pkg/event"
-	"github.com/crossplane/crossplane-runtime/pkg/logging"
 	"github.com/crossplane/crossplane-runtime/pkg/reconciler/providerconfig"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 
@@ -30,7 +30,7 @@ import (
 
 // Setup adds a controller that reconciles ProviderConfigs by accounting for
 // their current usage.
-func Setup(mgr ctrl.Manager, l logging.Logger) error {
+func Setup(mgr ctrl.Manager, o controller.Options) error {
 	name := providerconfig.ControllerName(v1alpha1.ProviderConfigGroupKind)
 
 	of := resource.ProviderConfigKinds{
@@ -43,6 +43,6 @@ func Setup(mgr ctrl.Manager, l logging.Logger) error {
 		For(&v1alpha1.ProviderConfig{}).
 		Watches(&source.Kind{Type: &v1alpha1.ProviderConfigUsage{}}, &resource.EnqueueRequestForProviderConfig{}).
 		Complete(providerconfig.NewReconciler(mgr, of,
-			providerconfig.WithLogger(l.WithValues("controller", name)),
+			providerconfig.WithLogger(o.Logger.WithValues("controller", name)),
 			providerconfig.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))))
 }
diff --git a/pkg/controller/postgresql/database/reconciler.go b/pkg/controller/postgresql/database/reconciler.go
index 403a2e1..c120f50 100644
--- a/pkg/controller/postgresql/database/reconciler.go
+++ b/pkg/controller/postgresql/database/reconciler.go
@@ -20,7 +20,6 @@ import (
 	"context"
 	"fmt"
 	"strings"
-	"time"
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
@@ -33,8 +32,8 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller"
 	"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/reconciler/managed"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
@@ -64,15 +63,15 @@ const (
 )
 
 // Setup adds a controller that reconciles Database managed resources.
-func Setup(mgr ctrl.Manager, l logging.Logger) error {
+func Setup(mgr ctrl.Manager, o xpcontroller.Options) error {
 	name := managed.ControllerName(v1alpha1.DatabaseGroupKind)
 
 	t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{})
 	r := managed.NewReconciler(mgr,
 		resource.ManagedKind(v1alpha1.DatabaseGroupVersionKind),
 		managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: postgresql.New}),
-		managed.WithLogger(l.WithValues("controller", name)),
-		managed.WithPollInterval(10*time.Minute),
+		managed.WithLogger(o.Logger.WithValues("controller", name)),
+		managed.WithPollInterval(o.PollInterval),
 		managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))))
 
 	return ctrl.NewControllerManagedBy(mgr).
diff --git a/pkg/controller/postgresql/extension/reconciler.go b/pkg/controller/postgresql/extension/reconciler.go
index 2a77cbb..6d82168 100644
--- a/pkg/controller/postgresql/extension/reconciler.go
+++ b/pkg/controller/postgresql/extension/reconciler.go
@@ -19,7 +19,6 @@ package extension
 import (
 	"context"
 	"strings"
-	"time"
 
 	"github.com/lib/pq"
 	"github.com/pkg/errors"
@@ -30,8 +29,8 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller"
 	"github.com/crossplane/crossplane-runtime/pkg/event"
-	"github.com/crossplane/crossplane-runtime/pkg/logging"
 	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 
@@ -56,15 +55,15 @@ const (
 )
 
 // Setup adds a controller that reconciles Extension managed resources.
-func Setup(mgr ctrl.Manager, l logging.Logger) error {
+func Setup(mgr ctrl.Manager, o xpcontroller.Options) error {
 	name := managed.ControllerName(v1alpha1.ExtensionGroupKind)
 
 	t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{})
 	r := managed.NewReconciler(mgr,
 		resource.ManagedKind(v1alpha1.ExtensionGroupVersionKind),
 		managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: postgresql.New}),
-		managed.WithLogger(l.WithValues("controller", name)),
-		managed.WithPollInterval(10*time.Minute),
+		managed.WithLogger(o.Logger.WithValues("controller", name)),
+		managed.WithPollInterval(o.PollInterval),
 		managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))))
 
 	return ctrl.NewControllerManagedBy(mgr).
diff --git a/pkg/controller/postgresql/grant/reconciler.go b/pkg/controller/postgresql/grant/reconciler.go
index 84d6d87..bb59cfc 100644
--- a/pkg/controller/postgresql/grant/reconciler.go
+++ b/pkg/controller/postgresql/grant/reconciler.go
@@ -20,7 +20,6 @@ import (
 	"context"
 	"fmt"
 	"strings"
-	"time"
 
 	"github.com/lib/pq"
 	"github.com/pkg/errors"
@@ -31,8 +30,8 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller"
 	"github.com/crossplane/crossplane-runtime/pkg/event"
-	"github.com/crossplane/crossplane-runtime/pkg/logging"
 	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 
@@ -65,15 +64,15 @@ const (
 )
 
 // Setup adds a controller that reconciles Grant managed resources.
-func Setup(mgr ctrl.Manager, l logging.Logger) error {
+func Setup(mgr ctrl.Manager, o xpcontroller.Options) error {
 	name := managed.ControllerName(v1alpha1.GrantGroupKind)
 
 	t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{})
 	r := managed.NewReconciler(mgr,
 		resource.ManagedKind(v1alpha1.GrantGroupVersionKind),
 		managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: postgresql.New}),
-		managed.WithLogger(l.WithValues("controller", name)),
-		managed.WithPollInterval(10*time.Minute),
+		managed.WithLogger(o.Logger.WithValues("controller", name)),
+		managed.WithPollInterval(o.PollInterval),
 		managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))))
 
 	return ctrl.NewControllerManagedBy(mgr).
diff --git a/pkg/controller/postgresql/postgresql.go b/pkg/controller/postgresql/postgresql.go
index 9b7d690..5eccf4d 100644
--- a/pkg/controller/postgresql/postgresql.go
+++ b/pkg/controller/postgresql/postgresql.go
@@ -19,7 +19,7 @@ package postgresql
 import (
 	ctrl "sigs.k8s.io/controller-runtime"
 
-	"github.com/crossplane/crossplane-runtime/pkg/logging"
+	"github.com/crossplane/crossplane-runtime/pkg/controller"
 
 	"github.com/crossplane-contrib/provider-sql/pkg/controller/postgresql/config"
 	"github.com/crossplane-contrib/provider-sql/pkg/controller/postgresql/database"
@@ -30,15 +30,15 @@ import (
 
 // Setup creates all PostgreSQL controllers with the supplied logger and adds
 // them to the supplied manager.
-func Setup(mgr ctrl.Manager, l logging.Logger) error {
-	for _, setup := range []func(ctrl.Manager, logging.Logger) error{
+func Setup(mgr ctrl.Manager, o controller.Options) error {
+	for _, setup := range []func(ctrl.Manager, controller.Options) error{
 		config.Setup,
 		database.Setup,
 		role.Setup,
 		grant.Setup,
 		extension.Setup,
 	} {
-		if err := setup(mgr, l); err != nil {
+		if err := setup(mgr, o); err != nil {
 			return err
 		}
 	}
diff --git a/pkg/controller/postgresql/role/reconciler.go b/pkg/controller/postgresql/role/reconciler.go
index a6c9218..474c93a 100644
--- a/pkg/controller/postgresql/role/reconciler.go
+++ b/pkg/controller/postgresql/role/reconciler.go
@@ -20,7 +20,6 @@ import (
 	"context"
 	"fmt"
 	"strings"
-	"time"
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
@@ -34,8 +33,8 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller"
 	"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/password"
 	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
@@ -66,15 +65,15 @@ const (
 )
 
 // Setup adds a controller that reconciles Role managed resources.
-func Setup(mgr ctrl.Manager, l logging.Logger) error {
+func Setup(mgr ctrl.Manager, o xpcontroller.Options) error {
 	name := managed.ControllerName(v1alpha1.RoleGroupKind)
 
 	t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{})
 	r := managed.NewReconciler(mgr,
 		resource.ManagedKind(v1alpha1.RoleGroupVersionKind),
 		managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: postgresql.New}),
-		managed.WithLogger(l.WithValues("controller", name)),
-		managed.WithPollInterval(10*time.Minute),
+		managed.WithLogger(o.Logger.WithValues("controller", name)),
+		managed.WithPollInterval(o.PollInterval),
 		managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))))
 
 	return ctrl.NewControllerManagedBy(mgr).
diff --git a/pkg/controller/sql.go b/pkg/controller/sql.go
index 1de3de8..cca8083 100644
--- a/pkg/controller/sql.go
+++ b/pkg/controller/sql.go
@@ -19,7 +19,7 @@ package controller
 import (
 	ctrl "sigs.k8s.io/controller-runtime"
 
-	"github.com/crossplane/crossplane-runtime/pkg/logging"
+	"github.com/crossplane/crossplane-runtime/pkg/controller"
 
 	"github.com/crossplane-contrib/provider-sql/pkg/controller/mssql"
 	"github.com/crossplane-contrib/provider-sql/pkg/controller/mysql"
@@ -28,8 +28,8 @@ import (
 
 // Setup creates all PostgreSQL controllers with the supplied logger and adds
 // them to the supplied manager.
-func Setup(mgr ctrl.Manager, l logging.Logger) error {
-	for _, setup := range []func(ctrl.Manager, logging.Logger) error{
+func Setup(mgr ctrl.Manager, l controller.Options) error {
+	for _, setup := range []func(ctrl.Manager, controller.Options) error{
 		mssql.Setup,
 		mysql.Setup,
 		postgresql.Setup,


Test patch:
diff --git a/cluster/local/integration_tests.sh b/cluster/local/integration_tests.sh
index 5737c92..5f21451 100755
--- a/cluster/local/integration_tests.sh
+++ b/cluster/local/integration_tests.sh
@@ -252,15 +252,15 @@ echo_step_completed
 
 echo_step "creating MySQL Grant resource"
 # create grant
-"${KUBECTL}" apply -f ${projectdir}/examples/mysql/grant.yaml
+"${KUBECTL}" apply -f ${projectdir}/examples/mysql/grant_database.yaml
 
 echo_info "check if is ready"
-"${KUBECTL}" wait --timeout 2m --for condition=Ready -f ${projectdir}/examples/mysql/grant.yaml
+"${KUBECTL}" wait --timeout 2m --for condition=Ready -f ${projectdir}/examples/mysql/grant_database.yaml
 echo_step_completed
 
 # uninstall
 echo_step "uninstalling ${PROJECT_NAME}"
-"${KUBECTL}" delete -f ${projectdir}/examples/mysql/grant.yaml
+"${KUBECTL}" delete -f ${projectdir}/examples/mysql/grant_database.yaml
 "${KUBECTL}" delete -f ${projectdir}/examples/mysql/database.yaml
 "${KUBECTL}" delete -f ${projectdir}/examples/mysql/user.yaml
 echo "${PROVIDER_CONFIG_YAML}" | "${KUBECTL}" delete -f -
diff --git a/pkg/controller/mysql/grant/reconciler_test.go b/pkg/controller/mysql/grant/reconciler_test.go
index 9955484..5417ccd 100644
--- a/pkg/controller/mysql/grant/reconciler_test.go
+++ b/pkg/controller/mysql/grant/reconciler_test.go
@@ -195,8 +195,9 @@ func TestObserve(t *testing.T) {
 	}
 
 	type want struct {
-		o   managed.ExternalObservation
-		err error
+		o                  managed.ExternalObservation
+		err                error
+		observedPrivileges []string
 	}
 
 	cases := map[string]struct {
@@ -215,7 +216,7 @@ func TestObserve(t *testing.T) {
 			},
 		},
 		"SuccessNoGrant": {
-			reason: "We should return ResourceExists: false when no grant is found",
+			reason: "We should return ResourceExists: false when no grant is found, being privileges result empty",
 			fields: fields{
 				db: mockDB{
 					MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) {
@@ -315,11 +316,12 @@ func TestObserve(t *testing.T) {
 					ResourceExists:   true,
 					ResourceUpToDate: true,
 				},
-				err: nil,
+				err:                nil,
+				observedPrivileges: []string{allPrivileges},
 			},
 		},
 		"SuccessDiffGrants": {
-			reason: "We should return no error if different grants exist",
+			reason: "We should return no error if different grants exist for the provided database",
 			fields: fields{
 				db: mockDB{
 					MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) {
@@ -347,6 +349,67 @@ func TestObserve(t *testing.T) {
 					ResourceExists:   true,
 					ResourceUpToDate: false,
 				},
+				observedPrivileges: []string{"CREATE"},
+			},
+		},
+		"SuccessDiffGrantNoDatabaseNoTable": {
+			reason: "We should return no error if different grants exist and no database and table are provided",
+			fields: fields{
+				db: mockDB{
+					MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) {
+						return mockRowsToSQLRows(
+							sqlmock.NewRows([]string{"Grants"}).
+								AddRow("GRANT INSERT ON *.* TO 'success-user'@%"),
+						), nil
+					},
+				},
+			},
+			args: args{
+				mg: &v1alpha1.Grant{
+					Spec: v1alpha1.GrantSpec{
+						ForProvider: v1alpha1.GrantParameters{
+							User:       pointer.StringPtr("success-user"),
+							Privileges: v1alpha1.GrantPrivileges{"DROP", "CREATE"},
+						},
+					},
+				},
+			},
+			want: want{
+				o: managed.ExternalObservation{
+					ResourceExists:   true,
+					ResourceUpToDate: false,
+				},
+				observedPrivileges: []string{"INSERT"},
+				err:                nil,
+			},
+		},
+		"SuccessDiffGrantUsage": {
+			reason: "We should return ResourceExists: false when a USAGE grant is found, since it is equivalent to having no grants",
+			fields: fields{
+				db: mockDB{
+					MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) {
+						return mockRowsToSQLRows(
+							sqlmock.NewRows([]string{"Grants"}).
+								AddRow("GRANT USAGE ON *.* TO 'success-user'@%"),
+						), nil
+					},
+				},
+			},
+			args: args{
+				mg: &v1alpha1.Grant{
+					Spec: v1alpha1.GrantSpec{
+						ForProvider: v1alpha1.GrantParameters{
+							User:       pointer.StringPtr("success-user"),
+							Privileges: v1alpha1.GrantPrivileges{"DROP", "CREATE"},
+						},
+					},
+				},
+			},
+			want: want{
+				o: managed.ExternalObservation{
+					ResourceExists: false,
+				},
+				err: nil,
 			},
 		},
 		"SuccessManyGrants": {
@@ -378,11 +441,12 @@ func TestObserve(t *testing.T) {
 					ResourceExists:   true,
 					ResourceUpToDate: true,
 				},
-				err: nil,
+				err:                nil,
+				observedPrivileges: []string{"CREATE", "DROP"},
 			},
 		},
 		"SuccessGrantNoDatabaseNoTable": {
-			reason: "We should return no error if no database and table were provided",
+			reason: "We should return no error if no database and table were provided and grants were equal to the ones in resource spec",
 			fields: fields{
 				db: mockDB{
 					MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) {
@@ -408,7 +472,8 @@ func TestObserve(t *testing.T) {
 					ResourceExists:   true,
 					ResourceUpToDate: true,
 				},
-				err: nil,
+				err:                nil,
+				observedPrivileges: []string{"CREATE", "DROP"},
 			},
 		},
 		"SuccessGrantWithTables": {
@@ -440,7 +505,8 @@ func TestObserve(t *testing.T) {
 					ResourceExists:   true,
 					ResourceUpToDate: true,
 				},
-				err: nil,
+				err:                nil,
+				observedPrivileges: []string{"CREATE", "DROP"},
 			},
 		},
 		"SuccessDiffGrantWithTables": {
@@ -472,7 +538,8 @@ func TestObserve(t *testing.T) {
 					ResourceExists:   true,
 					ResourceUpToDate: false,
 				},
-				err: nil,
+				err:                nil,
+				observedPrivileges: []string{"CREATE", "DROP"},
 			},
 		},
 	}
@@ -487,6 +554,13 @@ func TestObserve(t *testing.T) {
 			if diff := cmp.Diff(tc.want.o, got); diff != "" {
 				t.Errorf("\n%s\ne.Observe(...): -want, +got:\n%s\n", tc.reason, diff)
 			}
+
+			if tc.args.mg != nil {
+				cr, _ := tc.args.mg.(*v1alpha1.Grant)
+				if diff := cmp.Diff(tc.want.observedPrivileges, cr.Status.AtProvider.Privileges, equateSlices()...); diff != "" {
+					t.Errorf("\n%s\ne.Observe(...): -want, +got:\n%s\n", tc.reason, diff)
+				}
+			}
 		})
 	}
 }
@@ -613,19 +687,31 @@ func TestUpdate(t *testing.T) {
 				err: errors.New(errNotGrant),
 			},
 		},
-		"ErrExec": {
-			reason: "Any errors encountered while updating the grant should be returned",
+		"ErrExecRevokeNotRequired": {
+			reason: "Any errors encountered while revoking a not required privilege from the desired ones should be returned",
 			fields: fields{
 				db: &mockDB{
-					MockExec: func(ctx context.Context, q xsql.Query) error { return errBoom },
+					MockExec: func(ctx context.Context, q xsql.Query) error {
+						if strings.HasPrefix(q.String, "REVOKE") {
+							return errBoom
+						}
+
+						return nil
+					},
 				},
 			},
 			args: args{
 				mg: &v1alpha1.Grant{
 					Spec: v1alpha1.GrantSpec{
 						ForProvider: v1alpha1.GrantParameters{
-							Database: pointer.StringPtr("test-example"),
-							User:     pointer.StringPtr("test-example"),
+							Database:   pointer.StringPtr("test-example"),
+							User:       pointer.StringPtr("test-example"),
+							Privileges: v1alpha1.GrantPrivileges{"CREATE"},
+						},
+					},
+					Status: v1alpha1.GrantStatus{
+						AtProvider: v1alpha1.GrantObservation{
+							Privileges: []string{"INSERT", "CREATE"},
 						},
 					},
 				},
@@ -634,17 +720,44 @@ func TestUpdate(t *testing.T) {
 				err: errors.Wrap(errBoom, errRevokeGrant),
 			},
 		},
-		"Success": {
-			reason: "No error should be returned when we update a grant",
+		"ErrExecGrantMissing": {
+			reason: "Any errors encountered while granting a missing privilege from the desired ones should be returned",
 			fields: fields{
 				db: &mockDB{
 					MockExec: func(ctx context.Context, q xsql.Query) error {
-						if strings.HasPrefix(q.String, "REVOKE") || strings.HasPrefix(q.String, "FLUSH") {
-							return nil
-						}
-						if strings.Contains(q.String, "CREATE, DROP") {
-							return nil
+						if strings.HasPrefix(q.String, "GRANT") {
+							return errBoom
 						}
+
+						return nil
+					},
+				},
+			},
+			args: args{
+				mg: &v1alpha1.Grant{
+					Spec: v1alpha1.GrantSpec{
+						ForProvider: v1alpha1.GrantParameters{
+							Database:   pointer.StringPtr("test-example"),
+							User:       pointer.StringPtr("test-example"),
+							Privileges: v1alpha1.GrantPrivileges{"CREATE", "SELECT"},
+						},
+					},
+					Status: v1alpha1.GrantStatus{
+						AtProvider: v1alpha1.GrantObservation{
+							Privileges: []string{"CREATE"},
+						},
+					},
+				},
+			},
+			want: want{
+				err: errors.Wrap(errBoom, errCreateGrant),
+			},
+		},
+		"SuccessEqualObservedDesired": {
+			reason: "No query should be executed and no error should be returned when there is no diff between desired and observed privileges",
+			fields: fields{
+				db: &mockDB{
+					MockExec: func(ctx context.Context, q xsql.Query) error {
 						return errBoom
 					},
 				},
@@ -658,6 +771,79 @@ func TestUpdate(t *testing.T) {
 							Privileges: v1alpha1.GrantPrivileges{"CREATE", "DROP"},
 						},
 					},
+					Status: v1alpha1.GrantStatus{
+						AtProvider: v1alpha1.GrantObservation{
+							Privileges: []string{"DROP", "CREATE"},
+						},
+					},
+				},
+			},
+			want: want{
+				err: nil,
+				c:   managed.ExternalUpdate{},
+			},
+		},
+		"SuccessDiffObservedDesiredGrantMissing": {
+			reason: "No error should be returned when granting a missing privilege from the desired ones",
+			fields: fields{
+				db: &mockDB{
+					MockExec: func(ctx context.Context, q xsql.Query) error {
+						if strings.HasPrefix(q.String, "REVOKE") {
+							return errBoom
+						}
+
+						return nil
+					},
+				},
+			},
+			args: args{
+				mg: &v1alpha1.Grant{
+					Spec: v1alpha1.GrantSpec{
+						ForProvider: v1alpha1.GrantParameters{
+							Database:   pointer.StringPtr("test-example"),
+							User:       pointer.StringPtr("test-example"),
+							Privileges: v1alpha1.GrantPrivileges{"CREATE", "DROP"},
+						},
+					},
+					Status: v1alpha1.GrantStatus{
+						AtProvider: v1alpha1.GrantObservation{
+							Privileges: []string{"DROP"},
+						},
+					},
+				},
+			},
+			want: want{
+				err: nil,
+				c:   managed.ExternalUpdate{},
+			},
+		},
+		"SuccessDiffObservedDesiredRevokeNotRequired": {
+			reason: "No error should be returned when revoking a not required privilege from the desired ones",
+			fields: fields{
+				db: &mockDB{
+					MockExec: func(ctx context.Context, q xsql.Query) error {
+						if strings.HasPrefix(q.String, "GRANT") {
+							return errBoom
+						}
+
+						return nil
+					},
+				},
+			},
+			args: args{
+				mg: &v1alpha1.Grant{
+					Spec: v1alpha1.GrantSpec{
+						ForProvider: v1alpha1.GrantParameters{
+							Database:   pointer.StringPtr("test-example"),
+							User:       pointer.StringPtr("test-example"),
+							Privileges: v1alpha1.GrantPrivileges{"DROP"},
+						},
+					},
+					Status: v1alpha1.GrantStatus{
+						AtProvider: v1alpha1.GrantObservation{
+							Privileges: []string{"DROP", "SELECT"},
+						},
+					},
 				},
 			},
 			want: want{
@@ -792,3 +978,108 @@ func mockRowsToSQLRows(mockRows *sqlmock.Rows) *sql.Rows {
 	}
 	return rows
 }
+
+func Test_diffPermissions(t *testing.T) {
+	type args struct {
+		desired  []string
+		observed []string
+	}
+	type want struct {
+		toGrant  []string
+		toRevoke []string
+	}
+	cases := map[string]struct {
+		args
+		want
+	}{
+		"AsDesired": {
+			args: args{
+				desired:  []string{"CREATE TABLE", "DELETE"},
+				observed: []string{"CREATE TABLE", "DELETE"},
+			},
+			want: want{
+				toGrant:  nil,
+				toRevoke: nil,
+			},
+		},
+		"AsDesiredOrderNotMatter": {
+			args: args{
+				desired:  []string{"CREATE TABLE", "DELETE"},
+				observed: []string{"DELETE", "CREATE TABLE"},
+			},
+			want: want{
+				toGrant:  nil,
+				toRevoke: nil,
+			},
+		},
+		"NeedsGrant": {
+			args: args{
+				desired:  []string{"CREATE TABLE", "DELETE"},
+				observed: []string{"CREATE TABLE"},
+			},
+			want: want{
+				toGrant: []string{"DELETE"},
+			},
+		},
+		"NeedsRevoke": {
+			args: args{
+				desired:  []string{"CREATE TABLE"},
+				observed: []string{"CREATE TABLE", "DELETE"},
+			},
+			want: want{
+				toRevoke: []string{"DELETE"},
+			},
+		},
+		"NeedsBoth": {
+			args: args{
+				desired:  []string{"CREATE TABLE"},
+				observed: []string{"DELETE"},
+			},
+			want: want{
+				toGrant:  []string{"CREATE TABLE"},
+				toRevoke: []string{"DELETE"},
+			},
+		},
+		"GrantAll": {
+			args: args{
+				desired: []string{"CREATE TABLE", "DELETE", "INSERT"},
+			},
+			want: want{
+				toGrant: []string{"CREATE TABLE", "DELETE", "INSERT"},
+			},
+		},
+		"RevokeAll": {
+			args: args{
+				observed: []string{"CREATE TABLE", "DELETE", "INSERT"},
+			},
+			want: want{
+				toRevoke: []string{"CREATE TABLE", "DELETE", "INSERT"},
+			},
+		},
+	}
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			gotToGrant, gotToRevoke := diffPermissions(tc.args.desired, tc.args.observed)
+			if diff := cmp.Diff(tc.want.toGrant, gotToGrant, equateSlices()...); diff != "" {
+				t.Errorf("\ndiffPermissions(...): -want toGrant, +got toGrant:\n%s", diff)
+			}
+			if diff := cmp.Diff(tc.want.toRevoke, gotToRevoke, equateSlices()...); diff != "" {
+				t.Errorf("\ndiffPermissions(...): -want toRevoke, +got toRevoke:\n%s", diff)
+			}
+		})
+	}
+}
+
+func equateSlices() []cmp.Option {
+	return []cmp.Option{
+		cmp.Transformer("mapAllPrivileges", func(s string) string {
+			if s == "ALL PRIVILEGES" {
+				return "ALL"
+			}
+			return s
+		}),
+		cmpopts.SortSlices(func(x, y string) bool {
+			return x < y
+		}),
+	}
+}