From 1f6725c71ab1ba00bd8bf765b4701fd7220a7d93 Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Thu, 16 Feb 2023 16:29:36 -0300 Subject: [PATCH 01/19] Use auth plugin to create users and set its password if plugin requires it Signed-off-by: Alejandro Recalde --- apis/mysql/v1alpha1/user_types.go | 5 +++++ pkg/controller/mysql/user/reconciler.go | 11 +++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/apis/mysql/v1alpha1/user_types.go b/apis/mysql/v1alpha1/user_types.go index f34af6f4..e04cc00d 100644 --- a/apis/mysql/v1alpha1/user_types.go +++ b/apis/mysql/v1alpha1/user_types.go @@ -64,6 +64,11 @@ type ResourceOptions struct { // MaxUserConnections sets The number of simultaneous connections to the server by an account // +optional MaxUserConnections *int `json:"maxUserConnections,omitempty"` + + // AuthPlugin sets the mysql authentication plugin, defaults to mysql_native_password + // +optional + // +kubebuilder:validation:Pattern:=^([a-z]+_)+[a-z]+$ + AuthPlugin *string `json:"authPlugin,omitempty" default:"mysql_native_password"` } // A UserObservation represents the observed state of a MySQL user. diff --git a/pkg/controller/mysql/user/reconciler.go b/pkg/controller/mysql/user/reconciler.go index 8b3fff2d..0f0cb20c 100644 --- a/pkg/controller/mysql/user/reconciler.go +++ b/pkg/controller/mysql/user/reconciler.go @@ -240,6 +240,7 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext cr.SetConditions(xpv1.Creating()) username, host := mysql.SplitUserHost(meta.GetExternalName(cr)) + plugin := *cr.Spec.ForProvider.ResourceOptions.AuthPlugin pw, _, err := c.getPassword(ctx, cr) if err != nil { return managed.ExternalCreation{}, err @@ -257,11 +258,17 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext resourceOptions = fmt.Sprintf(" WITH %s", strings.Join(ro, " ")) } + password_section := "" + if plugin == "mysql_native_password" || plugin == "caching_sha2_password" { + password_section = fmt.Sprintf("AS %s", mysql.QuoteValue(pw)) + } + query := fmt.Sprintf( - "CREATE USER %s@%s IDENTIFIED BY %s%s", + "CREATE USER %s@%s IDENTIFIED WITH %s%s%s", mysql.QuoteValue(username), mysql.QuoteValue(host), - mysql.QuoteValue(pw), + mysql.QuoteValue(plugin), + password_section, resourceOptions, ) if err := c.db.Exec(ctx, xsql.Query{ From bc1e3ea088cef798eed1634bc59eb09bc1870bed Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Wed, 22 Feb 2023 16:15:47 -0300 Subject: [PATCH 02/19] Define default auth plugin and use password values in order to keep previous behavior working Signed-off-by: Alejandro Recalde --- apis/mysql/v1alpha1/user_types.go | 14 +- apis/mysql/v1alpha1/zz_generated.deepcopy.go | 10 ++ .../crds/mysql.sql.crossplane.io_users.yaml | 9 ++ pkg/controller/mysql/user/reconciler.go | 125 +++++++++++++----- pkg/controller/mysql/user/reconciler_test.go | 56 ++++++++ 5 files changed, 174 insertions(+), 40 deletions(-) diff --git a/apis/mysql/v1alpha1/user_types.go b/apis/mysql/v1alpha1/user_types.go index e04cc00d..e22425f2 100644 --- a/apis/mysql/v1alpha1/user_types.go +++ b/apis/mysql/v1alpha1/user_types.go @@ -45,6 +45,15 @@ type UserParameters struct { // See https://dev.mysql.com/doc/refman/8.0/en/user-resources.html // +optional ResourceOptions *ResourceOptions `json:"resourceOptions,omitempty"` + + // AuthPlugin sets the mysql authentication plugin, defaults to mysql_native_password + // +optional + // +kubebuilder:validation:Pattern:=^([a-z]+_)+[a-z]+$ + AuthPlugin *string `json:"authPlugin,omitempty" default:"mysql_native_password"` + + // UsePassword indicate whether the provided AuthPlugin requires setting a password, defaults to true + // +optional + UsePassword *bool `json:"usePassword,omitempty" default:"true"` } // ResourceOptions define the account specific resource limits. @@ -64,11 +73,6 @@ type ResourceOptions struct { // MaxUserConnections sets The number of simultaneous connections to the server by an account // +optional MaxUserConnections *int `json:"maxUserConnections,omitempty"` - - // AuthPlugin sets the mysql authentication plugin, defaults to mysql_native_password - // +optional - // +kubebuilder:validation:Pattern:=^([a-z]+_)+[a-z]+$ - AuthPlugin *string `json:"authPlugin,omitempty" default:"mysql_native_password"` } // A UserObservation represents the observed state of a MySQL user. diff --git a/apis/mysql/v1alpha1/zz_generated.deepcopy.go b/apis/mysql/v1alpha1/zz_generated.deepcopy.go index 1ef6357d..63820c64 100644 --- a/apis/mysql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/mysql/v1alpha1/zz_generated.deepcopy.go @@ -584,6 +584,16 @@ func (in *UserParameters) DeepCopyInto(out *UserParameters) { *out = new(ResourceOptions) (*in).DeepCopyInto(*out) } + if in.AuthPlugin != nil { + in, out := &in.AuthPlugin, &out.AuthPlugin + *out = new(string) + **out = **in + } + if in.UsePassword != nil { + in, out := &in.UsePassword, &out.UsePassword + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserParameters. diff --git a/package/crds/mysql.sql.crossplane.io_users.yaml b/package/crds/mysql.sql.crossplane.io_users.yaml index 06bfd10c..ae561238 100644 --- a/package/crds/mysql.sql.crossplane.io_users.yaml +++ b/package/crds/mysql.sql.crossplane.io_users.yaml @@ -62,6 +62,11 @@ spec: description: UserParameters define the desired state of a MySQL user instance. properties: + authPlugin: + description: AuthPlugin sets the mysql authentication plugin, + defaults to mysql_native_password + pattern: ^([a-z]+_)+[a-z]+$ + type: string passwordSecretRef: description: PasswordSecretRef references the secret that contains the password used for this user. If no reference is given, a @@ -102,6 +107,10 @@ spec: connections to the server by an account type: integer type: object + usePassword: + description: UsePassword indicate whether the provided AuthPlugin + requires setting a password, defaults to true + type: boolean type: object providerConfigRef: default: diff --git a/pkg/controller/mysql/user/reconciler.go b/pkg/controller/mysql/user/reconciler.go index 0f0cb20c..17201d62 100644 --- a/pkg/controller/mysql/user/reconciler.go +++ b/pkg/controller/mysql/user/reconciler.go @@ -240,55 +240,70 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext cr.SetConditions(xpv1.Creating()) username, host := mysql.SplitUserHost(meta.GetExternalName(cr)) - plugin := *cr.Spec.ForProvider.ResourceOptions.AuthPlugin - pw, _, err := c.getPassword(ctx, cr) - if err != nil { - return managed.ExternalCreation{}, err + plugin := defaultAuthPlugin(cr.Spec.ForProvider.AuthPlugin) + + var resourceOptions string + ro := resourceOptionsToClauses(cr.Spec.ForProvider.ResourceOptions) + if len(ro) != 0 { + resourceOptions = fmt.Sprintf(" WITH %s", strings.Join(ro, " ")) } - if pw == "" { - pw, err = password.Generate() + + if checkUsePassword(cr.Spec.ForProvider.UsePassword) { + pw, _, err := c.getPassword(ctx, cr) if err != nil { return managed.ExternalCreation{}, err } + if pw == "" { + pw, err = password.Generate() + if err != nil { + return managed.ExternalCreation{}, err + } + } + + if err := c.executeCreateUserQuery(ctx, username, host, plugin, resourceOptions, &pw); err != nil { + return managed.ExternalCreation{}, err + } + + return managed.ExternalCreation{ + ConnectionDetails: c.db.GetConnectionDetails(username, pw), + }, nil } - var resourceOptions string - ro := resourceOptionsToClauses(cr.Spec.ForProvider.ResourceOptions) - if len(ro) != 0 { - resourceOptions = fmt.Sprintf(" WITH %s", strings.Join(ro, " ")) + if err := c.executeCreateUserQuery(ctx, username, host, plugin, resourceOptions, nil); err != nil { + return managed.ExternalCreation{}, err } - password_section := "" - if plugin == "mysql_native_password" || plugin == "caching_sha2_password" { - password_section = fmt.Sprintf("AS %s", mysql.QuoteValue(pw)) + return managed.ExternalCreation{}, nil +} + +func (c *external) executeCreateUserQuery(ctx context.Context, username string, host string, plugin string, resourceOptions string, pw *string) error { + passwordSection := "" + if pw != nil { + passwordSection = fmt.Sprintf(" AS %s", mysql.QuoteValue(*pw)) } query := fmt.Sprintf( "CREATE USER %s@%s IDENTIFIED WITH %s%s%s", mysql.QuoteValue(username), mysql.QuoteValue(host), - mysql.QuoteValue(plugin), - password_section, + plugin, + passwordSection, resourceOptions, ) + if err := c.db.Exec(ctx, xsql.Query{ String: query, }); err != nil { - return managed.ExternalCreation{}, errors.Wrap(err, errCreateUser) + return errors.Wrap(err, errCreateUser) } + if err := c.db.Exec(ctx, xsql.Query{ String: "FLUSH PRIVILEGES", }); err != nil { - return managed.ExternalCreation{}, errors.Wrap(err, errFlushPriv) - } - - if len(ro) != 0 { - cr.Status.AtProvider.ResourceOptionsAsClauses = ro + return errors.Wrap(err, errFlushPriv) } - return managed.ExternalCreation{ - ConnectionDetails: c.db.GetConnectionDetails(username, pw), - }, nil + return nil } func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { @@ -298,10 +313,6 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext } username, host := mysql.SplitUserHost(meta.GetExternalName(cr)) - pw, pwchanged, err := c.getPassword(ctx, cr) - if err != nil { - return managed.ExternalUpdate{}, err - } ro := resourceOptionsToClauses(cr.Spec.ForProvider.ResourceOptions) rochanged, err := changedResourceOptions(cr.Status.AtProvider.ResourceOptionsAsClauses, ro) @@ -332,24 +343,68 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext cr.Status.AtProvider.ResourceOptionsAsClauses = ro } + if checkUsePassword(cr.Spec.ForProvider.UsePassword) { + connectionDetails, err := c.UpdatePassword(ctx, cr, username, host) + + if err != nil { + return managed.ExternalUpdate{}, err + } + + if len(connectionDetails) > 0 { + return managed.ExternalUpdate{ConnectionDetails: connectionDetails}, nil + } + } + + return managed.ExternalUpdate{}, nil +} + +func checkUsePassword(usePassword *bool) bool { + if usePassword == nil { + return true + } + + return *usePassword +} + +func (c *external) UpdatePassword(ctx context.Context, cr *v1alpha1.User, username, host string) (managed.ConnectionDetails, error) { + pw, pwchanged, err := c.getPassword(ctx, cr) + if err != nil { + return managed.ConnectionDetails{}, err + } + if pwchanged { - query := fmt.Sprintf("ALTER USER %s@%s IDENTIFIED BY %s", mysql.QuoteValue(username), mysql.QuoteValue(host), mysql.QuoteValue(pw)) + plugin := defaultAuthPlugin(cr.Spec.ForProvider.AuthPlugin) + query := fmt.Sprintf("ALTER USER %s@%s IDENTIFIED WITH %s AS %s", + mysql.QuoteValue(username), + mysql.QuoteValue(host), + plugin, + mysql.QuoteValue(pw), + ) + if err := c.db.Exec(ctx, xsql.Query{ String: query, }); err != nil { - return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser) + return managed.ConnectionDetails{}, errors.Wrap(err, errUpdateUser) } + if err := c.db.Exec(ctx, xsql.Query{ String: "FLUSH PRIVILEGES", }); err != nil { - return managed.ExternalUpdate{}, errors.Wrap(err, errFlushPriv) + return managed.ConnectionDetails{}, errors.Wrap(err, errFlushPriv) } - return managed.ExternalUpdate{ - ConnectionDetails: c.db.GetConnectionDetails(username, pw), - }, nil + return c.db.GetConnectionDetails(username, pw), nil } - return managed.ExternalUpdate{}, nil + + return managed.ConnectionDetails{}, nil +} + +func defaultAuthPlugin(authPlugin *string) string { + if authPlugin == nil { + return "mysql_native_password" + } + + return *authPlugin } func (c *external) Delete(ctx context.Context, mg resource.Managed) error { diff --git a/pkg/controller/mysql/user/reconciler_test.go b/pkg/controller/mysql/user/reconciler_test.go index da0374be..99c5c1e8 100644 --- a/pkg/controller/mysql/user/reconciler_test.go +++ b/pkg/controller/mysql/user/reconciler_test.go @@ -27,6 +27,7 @@ import ( "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" @@ -494,6 +495,34 @@ func TestCreate(t *testing.T) { }, }, }, + "UserWithAnAuthPluginThatNotRequiresPassword": { + reason: "A user that uses an authentication plugin that does not require password should not receive connection details and no error should happen", + comparePw: true, + fields: fields{ + db: &mockDB{ + MockExec: func(ctx context.Context, q xsql.Query) error { return nil }, + }, + }, + args: args{ + mg: &v1alpha1.User{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + meta.AnnotationKeyExternalName: "example", + }, + }, + Spec: v1alpha1.UserSpec{ + ForProvider: v1alpha1.UserParameters{ + AuthPlugin: pointer.StringPtr("authentication_ldap_simple"), + UsePassword: pointer.BoolPtr(false), + }, + }, + }, + }, + want: want{ + err: nil, + c: managed.ExternalCreation{}, + }, + }, } for name, tc := range cases { @@ -768,6 +797,33 @@ func TestUpdate(t *testing.T) { err: nil, }, }, + "UserWithAnAuthPluginThatNotRequiresPassword": { + reason: "A user that uses an authentication plugin that does not require password should not receive connection details and no error should happen", + fields: fields{ + db: &mockDB{ + MockExec: func(ctx context.Context, q xsql.Query) error { return nil }, + }, + }, + args: args{ + mg: &v1alpha1.User{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + meta.AnnotationKeyExternalName: "example", + }, + }, + Spec: v1alpha1.UserSpec{ + ForProvider: v1alpha1.UserParameters{ + AuthPlugin: pointer.StringPtr("authentication_ldap_simple"), + UsePassword: pointer.BoolPtr(false), + }, + }, + }, + }, + want: want{ + err: nil, + c: managed.ExternalUpdate{}, + }, + }, } for name, tc := range cases { From 0ab2e2285557741a02b1834822623a9e335a17a5 Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Wed, 22 Feb 2023 18:32:58 -0300 Subject: [PATCH 03/19] Add missing ResourceOptionsAsClauses cr assignation in user create Signed-off-by: Alejandro Recalde --- pkg/controller/mysql/user/reconciler.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/controller/mysql/user/reconciler.go b/pkg/controller/mysql/user/reconciler.go index 17201d62..47c7cde8 100644 --- a/pkg/controller/mysql/user/reconciler.go +++ b/pkg/controller/mysql/user/reconciler.go @@ -246,6 +246,7 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext ro := resourceOptionsToClauses(cr.Spec.ForProvider.ResourceOptions) if len(ro) != 0 { resourceOptions = fmt.Sprintf(" WITH %s", strings.Join(ro, " ")) + cr.Status.AtProvider.ResourceOptionsAsClauses = ro } if checkUsePassword(cr.Spec.ForProvider.UsePassword) { From c9e65f8a3f32c62e05863bbe28c85bc1a8841e4e Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Fri, 24 Feb 2023 17:13:55 -0300 Subject: [PATCH 04/19] Replace as with by in create and alter user queries Signed-off-by: Alejandro Recalde --- pkg/controller/mysql/user/reconciler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/controller/mysql/user/reconciler.go b/pkg/controller/mysql/user/reconciler.go index 47c7cde8..1a45c1a4 100644 --- a/pkg/controller/mysql/user/reconciler.go +++ b/pkg/controller/mysql/user/reconciler.go @@ -280,7 +280,7 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext func (c *external) executeCreateUserQuery(ctx context.Context, username string, host string, plugin string, resourceOptions string, pw *string) error { passwordSection := "" if pw != nil { - passwordSection = fmt.Sprintf(" AS %s", mysql.QuoteValue(*pw)) + passwordSection = fmt.Sprintf(" BY %s", mysql.QuoteValue(*pw)) } query := fmt.Sprintf( @@ -375,7 +375,7 @@ func (c *external) UpdatePassword(ctx context.Context, cr *v1alpha1.User, userna if pwchanged { plugin := defaultAuthPlugin(cr.Spec.ForProvider.AuthPlugin) - query := fmt.Sprintf("ALTER USER %s@%s IDENTIFIED WITH %s AS %s", + query := fmt.Sprintf("ALTER USER %s@%s IDENTIFIED WITH %s BY %s", mysql.QuoteValue(username), mysql.QuoteValue(host), plugin, From 2c6ba733b8e0347c4e3bb8c4818ee4ff6925dedb Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Fri, 24 Feb 2023 22:39:10 -0300 Subject: [PATCH 05/19] Set AuthPlugin as an user observation to be included in user status Signed-off-by: Alejandro Recalde --- apis/mysql/v1alpha1/user_types.go | 3 +++ apis/mysql/v1alpha1/zz_generated.deepcopy.go | 5 +++++ package/crds/mysql.sql.crossplane.io_users.yaml | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/apis/mysql/v1alpha1/user_types.go b/apis/mysql/v1alpha1/user_types.go index e22425f2..f554b764 100644 --- a/apis/mysql/v1alpha1/user_types.go +++ b/apis/mysql/v1alpha1/user_types.go @@ -79,6 +79,9 @@ type ResourceOptions struct { type UserObservation struct { // ResourceOptionsAsClauses represents the applied resource options ResourceOptionsAsClauses []string `json:"resourceOptionsAsClauses,omitempty"` + + // AuthPlugin represents the applied mysql authentication plugin + AuthPlugin *string `json:"authPlugin,omitempty"` } // +kubebuilder:object:root=true diff --git a/apis/mysql/v1alpha1/zz_generated.deepcopy.go b/apis/mysql/v1alpha1/zz_generated.deepcopy.go index 63820c64..e095088c 100644 --- a/apis/mysql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/mysql/v1alpha1/zz_generated.deepcopy.go @@ -559,6 +559,11 @@ func (in *UserObservation) DeepCopyInto(out *UserObservation) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.AuthPlugin != nil { + in, out := &in.AuthPlugin, &out.AuthPlugin + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserObservation. diff --git a/package/crds/mysql.sql.crossplane.io_users.yaml b/package/crds/mysql.sql.crossplane.io_users.yaml index ae561238..98a0b74a 100644 --- a/package/crds/mysql.sql.crossplane.io_users.yaml +++ b/package/crds/mysql.sql.crossplane.io_users.yaml @@ -290,6 +290,10 @@ spec: description: A UserObservation represents the observed state of a MySQL user. properties: + authPlugin: + description: AuthPlugin represents the applied mysql authentication + plugin + type: string resourceOptionsAsClauses: description: ResourceOptionsAsClauses represents the applied resource options From 3e710a694ba2581f59bbb14d1ddec33987675012 Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Sat, 25 Feb 2023 11:36:35 -0300 Subject: [PATCH 06/19] Include AuthPlugin in observed fields and apply alter query only of user spec field diverged Signed-off-by: Alejandro Recalde --- pkg/controller/mysql/user/reconciler.go | 163 ++++++++++++------- pkg/controller/mysql/user/reconciler_test.go | 136 +++++++++++++++- 2 files changed, 237 insertions(+), 62 deletions(-) diff --git a/pkg/controller/mysql/user/reconciler.go b/pkg/controller/mysql/user/reconciler.go index 1a45c1a4..d7550777 100644 --- a/pkg/controller/mysql/user/reconciler.go +++ b/pkg/controller/mysql/user/reconciler.go @@ -187,6 +187,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex username, host := mysql.SplitUserHost(meta.GetExternalName(cr)) observed := &v1alpha1.UserParameters{ + AuthPlugin: new(string), ResourceOptions: &v1alpha1.ResourceOptions{}, } @@ -195,6 +196,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex "max_updates, " + "max_connections, " + "max_user_connections " + + "plugin" + "FROM mysql.user WHERE User = ? AND Host = ?" err := c.db.Scan(ctx, xsql.Query{ @@ -208,6 +210,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex &observed.ResourceOptions.MaxUpdatesPerHour, &observed.ResourceOptions.MaxConnectionsPerHour, &observed.ResourceOptions.MaxUserConnections, + &observed.AuthPlugin, ) if xsql.IsNoRows(err) { return managed.ExternalObservation{ResourceExists: false}, nil @@ -222,6 +225,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex } cr.Status.AtProvider.ResourceOptionsAsClauses = resourceOptionsToClauses(observed.ResourceOptions) + cr.Status.AtProvider.AuthPlugin = observed.AuthPlugin cr.SetConditions(xpv1.Available()) @@ -242,14 +246,12 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext username, host := mysql.SplitUserHost(meta.GetExternalName(cr)) plugin := defaultAuthPlugin(cr.Spec.ForProvider.AuthPlugin) - var resourceOptions string ro := resourceOptionsToClauses(cr.Spec.ForProvider.ResourceOptions) if len(ro) != 0 { - resourceOptions = fmt.Sprintf(" WITH %s", strings.Join(ro, " ")) cr.Status.AtProvider.ResourceOptionsAsClauses = ro } - if checkUsePassword(cr.Spec.ForProvider.UsePassword) { + if checkUsePassword(cr) { pw, _, err := c.getPassword(ctx, cr) if err != nil { return managed.ExternalCreation{}, err @@ -261,7 +263,7 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext } } - if err := c.executeCreateUserQuery(ctx, username, host, plugin, resourceOptions, &pw); err != nil { + if err := c.executeCreateUserQuery(ctx, username, host, plugin, ro, &pw); err != nil { return managed.ExternalCreation{}, err } @@ -270,19 +272,24 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext }, nil } - if err := c.executeCreateUserQuery(ctx, username, host, plugin, resourceOptions, nil); err != nil { + if err := c.executeCreateUserQuery(ctx, username, host, plugin, ro, nil); err != nil { return managed.ExternalCreation{}, err } return managed.ExternalCreation{}, nil } -func (c *external) executeCreateUserQuery(ctx context.Context, username string, host string, plugin string, resourceOptions string, pw *string) error { +func (c *external) executeCreateUserQuery(ctx context.Context, username string, host string, plugin string, resourceOptionsClauses []string, pw *string) error { passwordSection := "" if pw != nil { passwordSection = fmt.Sprintf(" BY %s", mysql.QuoteValue(*pw)) } + resourceOptions := "" + if len(resourceOptionsClauses) != 0 { + resourceOptions = fmt.Sprintf(" WITH %s", strings.Join(resourceOptionsClauses, " ")) + } + query := fmt.Sprintf( "CREATE USER %s@%s IDENTIFIED WITH %s%s%s", mysql.QuoteValue(username), @@ -314,90 +321,124 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext } username, host := mysql.SplitUserHost(meta.GetExternalName(cr)) + plugin := defaultAuthPlugin(cr.Spec.ForProvider.AuthPlugin) - ro := resourceOptionsToClauses(cr.Spec.ForProvider.ResourceOptions) - rochanged, err := changedResourceOptions(cr.Status.AtProvider.ResourceOptionsAsClauses, ro) + roToAlter, err := getResourceOptionsToAlter(cr) if err != nil { - return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser) + return managed.ExternalUpdate{}, err } - if len(rochanged) > 0 { - resourceOptions := fmt.Sprintf("WITH %s", strings.Join(ro, " ")) + password, passwordChanged, err := getPassword(ctx, cr, c) + if err != nil { + return managed.ExternalUpdate{}, err + } - query := fmt.Sprintf( - "ALTER USER %s@%s %s", - mysql.QuoteValue(username), - mysql.QuoteValue(host), - resourceOptions, - ) - if err := c.db.Exec(ctx, xsql.Query{ - String: query, - }); err != nil { - return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser) - } - if err := c.db.Exec(ctx, xsql.Query{ - String: "FLUSH PRIVILEGES", - }); err != nil { - return managed.ExternalUpdate{}, errors.Wrap(err, errFlushPriv) + return c.applyAlterUserIfSomeFieldChanged(ctx, cr, passwordChanged, roToAlter, username, host, plugin, password) +} + +func (c *external) applyAlterUserIfSomeFieldChanged(ctx context.Context, cr *v1alpha1.User, passwordChanged bool, roToAlter []string, username string, host string, plugin string, password string) (managed.ExternalUpdate, error) { + if (checkUsePassword(cr) && passwordChanged) || checkAuthPluginChanged(cr) || checkResourceOptionsChanged(roToAlter) { + if err := c.executeAlterUserQuery(ctx, username, host, plugin, roToAlter, password); err != nil { + return managed.ExternalUpdate{}, err } + } - cr.Status.AtProvider.ResourceOptionsAsClauses = ro + if checkUsePassword(cr) && passwordChanged { + return managed.ExternalUpdate{ConnectionDetails: c.db.GetConnectionDetails(username, password)}, nil } - if checkUsePassword(cr.Spec.ForProvider.UsePassword) { - connectionDetails, err := c.UpdatePassword(ctx, cr, username, host) + return managed.ExternalUpdate{}, nil +} +func getPassword(ctx context.Context, cr *v1alpha1.User, c *external) (string, bool, error) { + password := "" + passwordChanged := false + if checkUsePassword(cr) { + pw, pwdChanged, err := c.getPassword(ctx, cr) if err != nil { - return managed.ExternalUpdate{}, err + return pw, pwdChanged, err } - if len(connectionDetails) > 0 { - return managed.ExternalUpdate{ConnectionDetails: connectionDetails}, nil - } + password = pw + passwordChanged = pwdChanged } - return managed.ExternalUpdate{}, nil + return password, passwordChanged, nil } -func checkUsePassword(usePassword *bool) bool { - if usePassword == nil { +func getResourceOptionsToAlter(cr *v1alpha1.User) ([]string, error) { + roToAlter := []string{} + + ro := resourceOptionsToClauses(cr.Spec.ForProvider.ResourceOptions) + roChanged, err := changedResourceOptions(cr.Status.AtProvider.ResourceOptionsAsClauses, ro) + if err != nil { + return roToAlter, errors.Wrap(err, errUpdateUser) + } + + if len(roChanged) > 0 { + cr.Status.AtProvider.ResourceOptionsAsClauses = ro + roToAlter = ro + } + + return roToAlter, nil +} + +func checkUsePassword(cr *v1alpha1.User) bool { + if cr.Spec.ForProvider.UsePassword == nil { return true } - return *usePassword + return *cr.Spec.ForProvider.UsePassword } -func (c *external) UpdatePassword(ctx context.Context, cr *v1alpha1.User, username, host string) (managed.ConnectionDetails, error) { - pw, pwchanged, err := c.getPassword(ctx, cr) - if err != nil { - return managed.ConnectionDetails{}, err +func checkResourceOptionsChanged(roToAlter []string) bool { + return len(roToAlter) > 0 +} + +func checkAuthPluginChanged(cr *v1alpha1.User) bool { + if cr.Status.AtProvider.AuthPlugin == nil { + return true } - if pwchanged { - plugin := defaultAuthPlugin(cr.Spec.ForProvider.AuthPlugin) - query := fmt.Sprintf("ALTER USER %s@%s IDENTIFIED WITH %s BY %s", - mysql.QuoteValue(username), - mysql.QuoteValue(host), - plugin, - mysql.QuoteValue(pw), - ) + if *cr.Status.AtProvider.AuthPlugin != defaultAuthPlugin(cr.Spec.ForProvider.AuthPlugin) { + return true + } - if err := c.db.Exec(ctx, xsql.Query{ - String: query, - }); err != nil { - return managed.ConnectionDetails{}, errors.Wrap(err, errUpdateUser) - } + return false +} - if err := c.db.Exec(ctx, xsql.Query{ - String: "FLUSH PRIVILEGES", - }); err != nil { - return managed.ConnectionDetails{}, errors.Wrap(err, errFlushPriv) - } +func (c *external) executeAlterUserQuery(ctx context.Context, username string, host string, plugin string, resourceOptionsClauses []string, pw string) error { + passwordSection := "" + if pw != "" { + passwordSection = fmt.Sprintf(" BY %s", mysql.QuoteValue(pw)) + } - return c.db.GetConnectionDetails(username, pw), nil + resourceOptions := "" + if len(resourceOptionsClauses) != 0 { + resourceOptions = fmt.Sprintf(" WITH %s", strings.Join(resourceOptionsClauses, " ")) } - return managed.ConnectionDetails{}, nil + query := fmt.Sprintf("ALTER USER %s@%s IDENTIFIED WITH %s%s%s", + mysql.QuoteValue(username), + mysql.QuoteValue(host), + plugin, + passwordSection, + resourceOptions, + ) + + if err := c.db.Exec(ctx, xsql.Query{ + String: query, + }); err != nil { + return errors.Wrap(err, errUpdateUser) + } + + if err := c.db.Exec(ctx, xsql.Query{ + String: "FLUSH PRIVILEGES", + }); err != nil { + return errors.Wrap(err, errFlushPriv) + } + + return nil } func defaultAuthPlugin(authPlugin *string) string { diff --git a/pkg/controller/mysql/user/reconciler_test.go b/pkg/controller/mysql/user/reconciler_test.go index 99c5c1e8..6ff29d38 100644 --- a/pkg/controller/mysql/user/reconciler_test.go +++ b/pkg/controller/mysql/user/reconciler_test.go @@ -603,6 +603,11 @@ func TestUpdate(t *testing.T) { }, }, }, + Status: v1alpha1.UserStatus{ + AtProvider: v1alpha1.UserObservation{ + AuthPlugin: pointer.StringPtr(defaultAuthPlugin(nil)), + }, + }, }, kube: &test.MockClient{ MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { @@ -633,6 +638,11 @@ func TestUpdate(t *testing.T) { meta.AnnotationKeyExternalName: "example", }, }, + Status: v1alpha1.UserStatus{ + AtProvider: v1alpha1.UserObservation{ + AuthPlugin: pointer.StringPtr(defaultAuthPlugin(nil)), + }, + }, }, }, want: want{ @@ -662,6 +672,11 @@ func TestUpdate(t *testing.T) { }, }, }, + Status: v1alpha1.UserStatus{ + AtProvider: v1alpha1.UserObservation{ + AuthPlugin: pointer.StringPtr(defaultAuthPlugin(nil)), + }, + }, }, kube: &test.MockClient{ MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { @@ -705,6 +720,11 @@ func TestUpdate(t *testing.T) { }, }, }, + Status: v1alpha1.UserStatus{ + AtProvider: v1alpha1.UserObservation{ + AuthPlugin: pointer.StringPtr(defaultAuthPlugin(nil)), + }, + }, }, kube: &test.MockClient{ MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { @@ -741,6 +761,65 @@ func TestUpdate(t *testing.T) { }, }, }, + "UpdatedResourceOptions": { + reason: "We should execute an SQL query if the resource options are not synced.", + fields: fields{ + db: &mockDB{ + MockExec: func(ctx context.Context, q xsql.Query) error { + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.User{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + meta.AnnotationKeyExternalName: "example", + }, + }, + Spec: v1alpha1.UserSpec{ + ForProvider: v1alpha1.UserParameters{ + PasswordSecretRef: &xpv1.SecretKeySelector{ + SecretReference: xpv1.SecretReference{ + Name: "connection-secret", + }, + Key: xpv1.ResourceCredentialsSecretPasswordKey, + }, + ResourceOptions: &v1alpha1.ResourceOptions{ + MaxQueriesPerHour: pointer.IntPtr(10), + MaxUpdatesPerHour: pointer.IntPtr(10), + MaxConnectionsPerHour: pointer.IntPtr(10), + MaxUserConnections: pointer.IntPtr(10), + }, + }, + }, + Status: v1alpha1.UserStatus{ + AtProvider: v1alpha1.UserObservation{ + ResourceOptionsAsClauses: []string{ + "MAX_QUERIES_PER_HOUR 20", // default ResourceOptions values + "MAX_UPDATES_PER_HOUR 20", + "MAX_CONNECTIONS_PER_HOUR 20", + "MAX_USER_CONNECTIONS 20", + }, + AuthPlugin: pointer.StringPtr(defaultAuthPlugin(nil)), // default AuthPlugin value + }, + }, + }, + kube: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + secret := corev1.Secret{ + Data: map[string][]byte{}, + } + secret.Data[xpv1.ResourceCredentialsSecretPasswordKey] = []byte("samesame") + secret.DeepCopyInto(obj.(*corev1.Secret)) + return nil + }, + }, + }, + want: want{ + err: nil, + }, + }, "NoUpdateQueryUnchangedResourceOptions": { reason: "We should not execute an SQL query if the resource options are unchanged.", fields: fields{ @@ -774,11 +853,12 @@ func TestUpdate(t *testing.T) { Status: v1alpha1.UserStatus{ AtProvider: v1alpha1.UserObservation{ ResourceOptionsAsClauses: []string{ - "MAX_QUERIES_PER_HOUR 0", + "MAX_QUERIES_PER_HOUR 0", // default ResourceOptions values "MAX_UPDATES_PER_HOUR 0", "MAX_CONNECTIONS_PER_HOUR 0", "MAX_USER_CONNECTIONS 0", }, + AuthPlugin: pointer.StringPtr(defaultAuthPlugin(nil)), // default AuthPlugin value }, }, }, @@ -817,6 +897,60 @@ func TestUpdate(t *testing.T) { UsePassword: pointer.BoolPtr(false), }, }, + Status: v1alpha1.UserStatus{ + AtProvider: v1alpha1.UserObservation{ + AuthPlugin: pointer.StringPtr("authentication_ldap_simple"), + }, + }, + }, + }, + want: want{ + err: nil, + c: managed.ExternalUpdate{}, + }, + }, + "UpdatedAuthPlugin": { + reason: "We should execute an SQL query if the auth plugin is not synced.", + fields: fields{ + db: &mockDB{ + MockExec: func(ctx context.Context, q xsql.Query) error { + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.User{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + meta.AnnotationKeyExternalName: "example", + }, + }, + Spec: v1alpha1.UserSpec{ + ForProvider: v1alpha1.UserParameters{ + PasswordSecretRef: &xpv1.SecretKeySelector{ + SecretReference: xpv1.SecretReference{ + Name: "connection-secret", + }, + Key: xpv1.ResourceCredentialsSecretPasswordKey, + }, + AuthPlugin: pointer.StringPtr(defaultAuthPlugin(nil)), + }, + }, + Status: v1alpha1.UserStatus{ + AtProvider: v1alpha1.UserObservation{ + AuthPlugin: pointer.StringPtr("authentication_ldap_simple"), + }, + }, + }, + kube: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + secret := corev1.Secret{ + Data: map[string][]byte{}, + } + secret.Data[xpv1.ResourceCredentialsSecretPasswordKey] = []byte("samesame") + secret.DeepCopyInto(obj.(*corev1.Secret)) + return nil + }, }, }, want: want{ From 6b453fe2574030b2c283c1a81dba529b277ee5b5 Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Mon, 27 Feb 2023 16:24:07 -0300 Subject: [PATCH 07/19] Add missing comma in grant select query (observe method) Signed-off-by: Alejandro Recalde --- pkg/controller/mysql/user/reconciler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/controller/mysql/user/reconciler.go b/pkg/controller/mysql/user/reconciler.go index d7550777..c2044827 100644 --- a/pkg/controller/mysql/user/reconciler.go +++ b/pkg/controller/mysql/user/reconciler.go @@ -195,7 +195,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex "max_questions, " + "max_updates, " + "max_connections, " + - "max_user_connections " + + "max_user_connections, " + "plugin" + "FROM mysql.user WHERE User = ? AND Host = ?" err := c.db.Scan(ctx, From 6529b9ea4a3b244ef926b0a00722a2b2b1e775f9 Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Mon, 27 Feb 2023 16:45:59 -0300 Subject: [PATCH 08/19] Add space in grant select query (observe method) Signed-off-by: Alejandro Recalde --- pkg/controller/mysql/user/reconciler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/controller/mysql/user/reconciler.go b/pkg/controller/mysql/user/reconciler.go index c2044827..5377d8ed 100644 --- a/pkg/controller/mysql/user/reconciler.go +++ b/pkg/controller/mysql/user/reconciler.go @@ -196,7 +196,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex "max_updates, " + "max_connections, " + "max_user_connections, " + - "plugin" + + "plugin " + "FROM mysql.user WHERE User = ? AND Host = ?" err := c.db.Scan(ctx, xsql.Query{ From e71cc6ec91e28ccc78b85a02bde92c84cd2d8157 Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Sat, 18 Oct 2025 20:57:15 -0300 Subject: [PATCH 09/19] refactor: apply chlunde suggestion of using either nil or empty string as default plugin > Instead of mysql_native_password that has been deprecated from MySQL 8.0.34 Signed-off-by: Alejandro Recalde --- .../cluster/mysql/user/reconciler.go | 54 ++++++++++++++----- .../cluster/mysql/user/reconciler_test.go | 21 ++++---- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/pkg/controller/cluster/mysql/user/reconciler.go b/pkg/controller/cluster/mysql/user/reconciler.go index 0b8e0449..bb7f67ed 100644 --- a/pkg/controller/cluster/mysql/user/reconciler.go +++ b/pkg/controller/cluster/mysql/user/reconciler.go @@ -317,9 +317,20 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext } func (c *external) executeCreateUserQuery(ctx context.Context, username string, host string, plugin string, resourceOptionsClauses []string, pw *string) error { - passwordSection := "" - if pw != nil { - passwordSection = fmt.Sprintf(" BY %s", mysql.QuoteValue(*pw)) + var identifiedClause string + + // When plugin is empty (nil or ""), use default auth: IDENTIFIED BY 'password' + // When plugin is specified, use: IDENTIFIED WITH plugin BY 'password' + // This ensures compatibility with both MySQL and MariaDB + if plugin == "" { + if pw != nil { + identifiedClause = fmt.Sprintf("IDENTIFIED BY %s", mysql.QuoteValue(*pw)) + } + } else { + identifiedClause = fmt.Sprintf("IDENTIFIED WITH %s", plugin) + if pw != nil { + identifiedClause += fmt.Sprintf(" BY %s", mysql.QuoteValue(*pw)) + } } resourceOptions := "" @@ -328,11 +339,10 @@ func (c *external) executeCreateUserQuery(ctx context.Context, username string, } query := fmt.Sprintf( - "CREATE USER %s@%s IDENTIFIED WITH %s%s%s", + "CREATE USER %s@%s %s%s", mysql.QuoteValue(username), mysql.QuoteValue(host), - plugin, - passwordSection, + identifiedClause, resourceOptions, ) @@ -396,16 +406,29 @@ func (c *external) applyAlterUserIfSomeFieldChanged(ctx context.Context, cr *v1a } func (c *external) executeAlterUserQuery(ctx context.Context, username string, host string, plugin string, pw string) error { - passwordSection := "" - if pw != "" { - passwordSection = fmt.Sprintf(" BY %s", mysql.QuoteValue(pw)) + var identifiedClause string + + // When plugin is empty (nil or ""), use default auth: IDENTIFIED BY 'password' + // When plugin is specified, use: IDENTIFIED WITH plugin BY 'password' + // This ensures compatibility with both MySQL and MariaDB + if plugin == "" { + if pw != "" { + identifiedClause = fmt.Sprintf("IDENTIFIED BY %s", mysql.QuoteValue(pw)) + } else { + // No password and no plugin means nothing to update + return nil + } + } else { + identifiedClause = fmt.Sprintf("IDENTIFIED WITH %s", plugin) + if pw != "" { + identifiedClause += fmt.Sprintf(" BY %s", mysql.QuoteValue(pw)) + } } - query := fmt.Sprintf("ALTER USER %s@%s IDENTIFIED WITH %s%s", + query := fmt.Sprintf("ALTER USER %s@%s %s", mysql.QuoteValue(username), mysql.QuoteValue(host), - plugin, - passwordSection, + identifiedClause, ) if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errUpdateUser}); err != nil { @@ -472,8 +495,11 @@ func checkAuthPluginChanged(cr *v1alpha1.User) bool { } func defaultAuthPlugin(authPlugin *string) string { - if authPlugin == nil { - return "mysql_native_password" + // nil or empty string means use the default plugin (let MySQL/MariaDB decide) + // This avoids hardcoding mysql_native_password which is deprecated in MySQL 8.0.34+ + // and not supported in MariaDB + if authPlugin == nil || *authPlugin == "" { + return "" } return *authPlugin diff --git a/pkg/controller/cluster/mysql/user/reconciler_test.go b/pkg/controller/cluster/mysql/user/reconciler_test.go index b513183c..0d2f92ac 100644 --- a/pkg/controller/cluster/mysql/user/reconciler_test.go +++ b/pkg/controller/cluster/mysql/user/reconciler_test.go @@ -255,11 +255,12 @@ func TestObserve(t *testing.T) { fields: fields{ db: mockDB{ MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { - // Set the auth plugin to the default value to match what the database would return + // Set the auth plugin to empty string (database will use its default) + // This avoids hardcoding mysql_native_password which is deprecated/removed if len(dest) >= 5 { if plugin, ok := dest[4].(**string); ok { - defaultPlugin := "mysql_native_password" - *plugin = &defaultPlugin + emptyPlugin := "" + *plugin = &emptyPlugin } } return nil @@ -627,7 +628,7 @@ func TestUpdate(t *testing.T) { }, Status: v1alpha1.UserStatus{ AtProvider: v1alpha1.UserObservation{ - AuthPlugin: pointer.StringPtr(defaultAuthPlugin(nil)), + AuthPlugin: pointer.StringPtr(""), }, }, }, @@ -662,7 +663,7 @@ func TestUpdate(t *testing.T) { }, Status: v1alpha1.UserStatus{ AtProvider: v1alpha1.UserObservation{ - AuthPlugin: pointer.StringPtr(defaultAuthPlugin(nil)), + AuthPlugin: pointer.StringPtr(""), }, }, }, @@ -696,7 +697,7 @@ func TestUpdate(t *testing.T) { }, Status: v1alpha1.UserStatus{ AtProvider: v1alpha1.UserObservation{ - AuthPlugin: pointer.StringPtr(defaultAuthPlugin(nil)), + AuthPlugin: pointer.StringPtr(""), }, }, }, @@ -744,7 +745,7 @@ func TestUpdate(t *testing.T) { }, Status: v1alpha1.UserStatus{ AtProvider: v1alpha1.UserObservation{ - AuthPlugin: pointer.StringPtr(defaultAuthPlugin(nil)), + AuthPlugin: pointer.StringPtr(""), }, }, }, @@ -823,7 +824,7 @@ func TestUpdate(t *testing.T) { "MAX_CONNECTIONS_PER_HOUR 20", "MAX_USER_CONNECTIONS 20", }, - AuthPlugin: pointer.StringPtr(defaultAuthPlugin(nil)), // default AuthPlugin value + AuthPlugin: pointer.StringPtr(""), // default AuthPlugin value }, }, }, @@ -886,7 +887,7 @@ func TestUpdate(t *testing.T) { "MAX_CONNECTIONS_PER_HOUR 0", "MAX_USER_CONNECTIONS 0", }, - AuthPlugin: pointer.StringPtr(defaultAuthPlugin(nil)), // default AuthPlugin value + AuthPlugin: pointer.StringPtr(""), // default AuthPlugin value }, }, }, @@ -961,7 +962,7 @@ func TestUpdate(t *testing.T) { }, Key: xpv1.ResourceCredentialsSecretPasswordKey, }, - AuthPlugin: pointer.StringPtr(defaultAuthPlugin(nil)), + AuthPlugin: pointer.StringPtr(""), }, }, Status: v1alpha1.UserStatus{ From 89c48e46a84396acc5a2552c72f3c07a198bb37c Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Sat, 18 Oct 2025 21:07:34 -0300 Subject: [PATCH 10/19] refactor: replace pointer package usage with ptr package Signed-off-by: Alejandro Recalde --- .../cluster/mysql/user/reconciler.go | 4 +-- .../cluster/mysql/user/reconciler_test.go | 36 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/pkg/controller/cluster/mysql/user/reconciler.go b/pkg/controller/cluster/mysql/user/reconciler.go index bb7f67ed..fbf6d68b 100644 --- a/pkg/controller/cluster/mysql/user/reconciler.go +++ b/pkg/controller/cluster/mysql/user/reconciler.go @@ -318,7 +318,7 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext func (c *external) executeCreateUserQuery(ctx context.Context, username string, host string, plugin string, resourceOptionsClauses []string, pw *string) error { var identifiedClause string - + // When plugin is empty (nil or ""), use default auth: IDENTIFIED BY 'password' // When plugin is specified, use: IDENTIFIED WITH plugin BY 'password' // This ensures compatibility with both MySQL and MariaDB @@ -407,7 +407,7 @@ func (c *external) applyAlterUserIfSomeFieldChanged(ctx context.Context, cr *v1a func (c *external) executeAlterUserQuery(ctx context.Context, username string, host string, plugin string, pw string) error { var identifiedClause string - + // When plugin is empty (nil or ""), use default auth: IDENTIFIED BY 'password' // When plugin is specified, use: IDENTIFIED WITH plugin BY 'password' // This ensures compatibility with both MySQL and MariaDB diff --git a/pkg/controller/cluster/mysql/user/reconciler_test.go b/pkg/controller/cluster/mysql/user/reconciler_test.go index 0d2f92ac..ade61a63 100644 --- a/pkg/controller/cluster/mysql/user/reconciler_test.go +++ b/pkg/controller/cluster/mysql/user/reconciler_test.go @@ -28,7 +28,7 @@ import ( "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/pointer" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" @@ -529,8 +529,8 @@ func TestCreate(t *testing.T) { }, Spec: v1alpha1.UserSpec{ ForProvider: v1alpha1.UserParameters{ - AuthPlugin: pointer.StringPtr("authentication_ldap_simple"), - UsePassword: pointer.BoolPtr(false), + AuthPlugin: ptr.To("authentication_ldap_simple"), + UsePassword: ptr.To(false), }, }, }, @@ -628,7 +628,7 @@ func TestUpdate(t *testing.T) { }, Status: v1alpha1.UserStatus{ AtProvider: v1alpha1.UserObservation{ - AuthPlugin: pointer.StringPtr(""), + AuthPlugin: ptr.To(""), }, }, }, @@ -663,7 +663,7 @@ func TestUpdate(t *testing.T) { }, Status: v1alpha1.UserStatus{ AtProvider: v1alpha1.UserObservation{ - AuthPlugin: pointer.StringPtr(""), + AuthPlugin: ptr.To(""), }, }, }, @@ -697,7 +697,7 @@ func TestUpdate(t *testing.T) { }, Status: v1alpha1.UserStatus{ AtProvider: v1alpha1.UserObservation{ - AuthPlugin: pointer.StringPtr(""), + AuthPlugin: ptr.To(""), }, }, }, @@ -745,7 +745,7 @@ func TestUpdate(t *testing.T) { }, Status: v1alpha1.UserStatus{ AtProvider: v1alpha1.UserObservation{ - AuthPlugin: pointer.StringPtr(""), + AuthPlugin: ptr.To(""), }, }, }, @@ -809,10 +809,10 @@ func TestUpdate(t *testing.T) { Key: xpv1.ResourceCredentialsSecretPasswordKey, }, ResourceOptions: &v1alpha1.ResourceOptions{ - MaxQueriesPerHour: pointer.IntPtr(10), - MaxUpdatesPerHour: pointer.IntPtr(10), - MaxConnectionsPerHour: pointer.IntPtr(10), - MaxUserConnections: pointer.IntPtr(10), + MaxQueriesPerHour: ptr.To(10), + MaxUpdatesPerHour: ptr.To(10), + MaxConnectionsPerHour: ptr.To(10), + MaxUserConnections: ptr.To(10), }, }, }, @@ -824,7 +824,7 @@ func TestUpdate(t *testing.T) { "MAX_CONNECTIONS_PER_HOUR 20", "MAX_USER_CONNECTIONS 20", }, - AuthPlugin: pointer.StringPtr(""), // default AuthPlugin value + AuthPlugin: ptr.To(""), // default AuthPlugin value }, }, }, @@ -887,7 +887,7 @@ func TestUpdate(t *testing.T) { "MAX_CONNECTIONS_PER_HOUR 0", "MAX_USER_CONNECTIONS 0", }, - AuthPlugin: pointer.StringPtr(""), // default AuthPlugin value + AuthPlugin: ptr.To(""), // default AuthPlugin value }, }, }, @@ -922,13 +922,13 @@ func TestUpdate(t *testing.T) { }, Spec: v1alpha1.UserSpec{ ForProvider: v1alpha1.UserParameters{ - AuthPlugin: pointer.StringPtr("authentication_ldap_simple"), - UsePassword: pointer.BoolPtr(false), + AuthPlugin: ptr.To("authentication_ldap_simple"), + UsePassword: ptr.To(false), }, }, Status: v1alpha1.UserStatus{ AtProvider: v1alpha1.UserObservation{ - AuthPlugin: pointer.StringPtr("authentication_ldap_simple"), + AuthPlugin: ptr.To("authentication_ldap_simple"), }, }, }, @@ -962,12 +962,12 @@ func TestUpdate(t *testing.T) { }, Key: xpv1.ResourceCredentialsSecretPasswordKey, }, - AuthPlugin: pointer.StringPtr(""), + AuthPlugin: ptr.To(""), }, }, Status: v1alpha1.UserStatus{ AtProvider: v1alpha1.UserObservation{ - AuthPlugin: pointer.StringPtr("authentication_ldap_simple"), + AuthPlugin: ptr.To("authentication_ldap_simple"), }, }, }, From 0f1c5e60933161927b1525a74a0348ee98e6b707 Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Sun, 19 Oct 2025 17:27:16 -0300 Subject: [PATCH 11/19] docs: update authPlugin field from user spec to use database default instead of mysql_native_password Signed-off-by: Alejandro Recalde --- apis/cluster/mysql/v1alpha1/user_types.go | 7 +++++-- package/crds/mysql.sql.crossplane.io_users.yaml | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apis/cluster/mysql/v1alpha1/user_types.go b/apis/cluster/mysql/v1alpha1/user_types.go index c2ddb569..cf5f9f55 100644 --- a/apis/cluster/mysql/v1alpha1/user_types.go +++ b/apis/cluster/mysql/v1alpha1/user_types.go @@ -46,10 +46,13 @@ type UserParameters struct { // +optional ResourceOptions *ResourceOptions `json:"resourceOptions,omitempty"` - // AuthPlugin sets the mysql authentication plugin, defaults to mysql_native_password + // AuthPlugin sets the mysql authentication plugin. + // If not specified (nil or empty string), the database server's default authentication plugin is used. + // This allows compatibility with different MySQL/MariaDB versions and their default authentication methods. + // Common plugins: caching_sha2_password (MySQL 8.0+), mysql_native_password, authentication_ldap_simple, etc. // +optional // +kubebuilder:validation:Pattern:=^([a-z]+_)+[a-z]+$ - AuthPlugin *string `json:"authPlugin,omitempty" default:"mysql_native_password"` + AuthPlugin *string `json:"authPlugin,omitempty"` // UsePassword indicate whether the provided AuthPlugin requires setting a password, defaults to true // +optional diff --git a/package/crds/mysql.sql.crossplane.io_users.yaml b/package/crds/mysql.sql.crossplane.io_users.yaml index 25280175..532979f5 100644 --- a/package/crds/mysql.sql.crossplane.io_users.yaml +++ b/package/crds/mysql.sql.crossplane.io_users.yaml @@ -72,8 +72,11 @@ spec: instance. properties: authPlugin: - description: AuthPlugin sets the mysql authentication plugin, - defaults to mysql_native_password + description: |- + AuthPlugin sets the mysql authentication plugin. + If not specified (nil or empty string), the database server's default authentication plugin is used. + This allows compatibility with different MySQL/MariaDB versions and their default authentication methods. + Common plugins: caching_sha2_password (MySQL 8.0+), mysql_native_password, authentication_ldap_simple, etc. pattern: ^([a-z]+_)+[a-z]+$ type: string binlog: From 7f0f5a2ee50aa9f8f39e08ca38f1c2e33f30b9bb Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Sun, 19 Oct 2025 17:28:14 -0300 Subject: [PATCH 12/19] docs: add examples for MySQL users with different authentication plugins Signed-off-by: Alejandro Recalde --- .../cluster/mysql/user_with_auth_plugin.yaml | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 examples/cluster/mysql/user_with_auth_plugin.yaml diff --git a/examples/cluster/mysql/user_with_auth_plugin.yaml b/examples/cluster/mysql/user_with_auth_plugin.yaml new file mode 100644 index 00000000..5a051ce6 --- /dev/null +++ b/examples/cluster/mysql/user_with_auth_plugin.yaml @@ -0,0 +1,52 @@ +--- +# Example: User with custom authentication plugin (e.g., LDAP) +# Some authentication plugins like authentication_ldap_simple don't require +# passwords, so usePassword is set to false +apiVersion: mysql.sql.crossplane.io/v1alpha1 +kind: User +metadata: + name: example-ldap-user +spec: + forProvider: + authPlugin: authentication_ldap_simple + usePassword: false # LDAP authentication doesn't use a MySQL password + providerConfigRef: + name: example +--- +# Example: User with specific authentication plugin and password +# For plugins that require passwords like caching_sha2_password +apiVersion: mysql.sql.crossplane.io/v1alpha1 +kind: User +metadata: + name: example-sha2-user +spec: + forProvider: + authPlugin: caching_sha2_password + passwordSecretRef: + name: example-pw + namespace: default + key: password + writeConnectionSecretToRef: + name: example-sha2-connection-secret + namespace: default + providerConfigRef: + name: example +--- +# Example: User without authPlugin specified (uses database server default) +# This is the recommended approach for maximum compatibility +# across different MySQL/MariaDB versions +apiVersion: mysql.sql.crossplane.io/v1alpha1 +kind: User +metadata: + name: example-default-user +spec: + forProvider: + passwordSecretRef: + name: example-pw + namespace: default + key: password + writeConnectionSecretToRef: + name: example-default-connection-secret + namespace: default + providerConfigRef: + name: example From 5f01d39f3f3f6f5e66d0e944311fc95bd5b51791 Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Sun, 19 Oct 2025 17:46:41 -0300 Subject: [PATCH 13/19] docs: correct example user name for caching_sha2_password authentication Signed-off-by: Alejandro Recalde --- examples/cluster/mysql/user_with_auth_plugin.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cluster/mysql/user_with_auth_plugin.yaml b/examples/cluster/mysql/user_with_auth_plugin.yaml index 5a051ce6..a0bb5488 100644 --- a/examples/cluster/mysql/user_with_auth_plugin.yaml +++ b/examples/cluster/mysql/user_with_auth_plugin.yaml @@ -18,7 +18,7 @@ spec: apiVersion: mysql.sql.crossplane.io/v1alpha1 kind: User metadata: - name: example-sha2-user + name: example-caching-sha2-user spec: forProvider: authPlugin: caching_sha2_password From f691e40abbbaa07423a99b538d0c1d15bef1f671 Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Sun, 19 Oct 2025 19:21:56 -0300 Subject: [PATCH 14/19] refactor(mysql): streamline password handling and user creation/update logic Signed-off-by: Alejandro Recalde --- .../cluster/mysql/user/reconciler.go | 180 ++++++++---------- 1 file changed, 83 insertions(+), 97 deletions(-) diff --git a/pkg/controller/cluster/mysql/user/reconciler.go b/pkg/controller/cluster/mysql/user/reconciler.go index fbf6d68b..19e3be1f 100644 --- a/pkg/controller/cluster/mysql/user/reconciler.go +++ b/pkg/controller/cluster/mysql/user/reconciler.go @@ -276,36 +276,25 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext username, host := mysql.SplitUserHost(meta.GetExternalName(cr)) plugin := defaultAuthPlugin(cr.Spec.ForProvider.AuthPlugin) - - pw, _, err := c.getPassword(ctx, cr) - if err != nil { - return managed.ExternalCreation{}, err - } - - if pw == "" { - pw, err = password.Generate() - if err != nil { - return managed.ExternalCreation{}, err - } - } - ro := resourceOptionsToClauses(cr.Spec.ForProvider.ResourceOptions) + var pw *string if checkUsePassword(cr) { - if err := c.executeCreateUserQuery(ctx, username, host, plugin, ro, &pw); err != nil { + userPassword, _, err := c.getPassword(ctx, cr) + if err != nil { return managed.ExternalCreation{}, err } - if len(ro) != 0 { - cr.Status.AtProvider.ResourceOptionsAsClauses = ro + if userPassword == "" { + userPassword, err = password.Generate() + if err != nil { + return managed.ExternalCreation{}, err + } } - - return managed.ExternalCreation{ - ConnectionDetails: c.db.GetConnectionDetails(username, pw), - }, nil + pw = &userPassword } - if err := c.executeCreateUserQuery(ctx, username, host, plugin, ro, nil); err != nil { + if err := c.executeCreateUserQuery(ctx, username, host, plugin, ro, pw); err != nil { return managed.ExternalCreation{}, err } @@ -313,25 +302,17 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext cr.Status.AtProvider.ResourceOptionsAsClauses = ro } + if pw != nil { + return managed.ExternalCreation{ + ConnectionDetails: c.db.GetConnectionDetails(username, *pw), + }, nil + } + return managed.ExternalCreation{}, nil } func (c *external) executeCreateUserQuery(ctx context.Context, username string, host string, plugin string, resourceOptionsClauses []string, pw *string) error { - var identifiedClause string - - // When plugin is empty (nil or ""), use default auth: IDENTIFIED BY 'password' - // When plugin is specified, use: IDENTIFIED WITH plugin BY 'password' - // This ensures compatibility with both MySQL and MariaDB - if plugin == "" { - if pw != nil { - identifiedClause = fmt.Sprintf("IDENTIFIED BY %s", mysql.QuoteValue(*pw)) - } - } else { - identifiedClause = fmt.Sprintf("IDENTIFIED WITH %s", plugin) - if pw != nil { - identifiedClause += fmt.Sprintf(" BY %s", mysql.QuoteValue(*pw)) - } - } + identifiedClause := buildIdentifiedClause(plugin, pw) resourceOptions := "" if len(resourceOptionsClauses) != 0 { @@ -346,11 +327,7 @@ func (c *external) executeCreateUserQuery(ctx context.Context, username string, resourceOptions, ) - if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errCreateUser}); err != nil { - return err - } - - return nil + return mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errCreateUser}) } func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { @@ -367,38 +344,61 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalUpdate{}, err } - password, passwordChanged, err := getPassword(ctx, cr, c) - if err != nil { + password := "" + passwordChanged := false + if checkUsePassword(cr) { + password, passwordChanged, err = c.getPassword(ctx, cr) + if err != nil { + return managed.ExternalUpdate{}, err + } + } + + return c.applyUserChanges(ctx, cr, passwordChanged, roToAlter, username, host, plugin, password) +} + +func (c *external) applyUserChanges(ctx context.Context, cr *v1alpha1.User, passwordChanged bool, roToAlter []string, username string, host string, plugin string, password string) (managed.ExternalUpdate, error) { + // Handle resource options changes (separate ALTER USER statement) + if err := c.updateResourceOptionsIfChanged(ctx, cr, roToAlter, username, host); err != nil { return managed.ExternalUpdate{}, err } - return c.applyAlterUserIfSomeFieldChanged(ctx, cr, passwordChanged, roToAlter, username, host, plugin, password) + // Handle auth plugin and/or password changes (separate ALTER USER statement) + return c.updateAuthIfChanged(ctx, cr, passwordChanged, username, host, plugin, password) } -func (c *external) applyAlterUserIfSomeFieldChanged(ctx context.Context, cr *v1alpha1.User, passwordChanged bool, roToAlter []string, username string, host string, plugin string, password string) (managed.ExternalUpdate, error) { - // Handle resource options changes - if checkResourceOptionsChanged(roToAlter) { - resourceOptions := fmt.Sprintf(" WITH %s", strings.Join(roToAlter, " ")) - query := fmt.Sprintf( - "ALTER USER %s@%s%s", - mysql.QuoteValue(username), - mysql.QuoteValue(host), - resourceOptions, - ) - if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errUpdateUser}); err != nil { - return managed.ExternalUpdate{}, err - } - cr.Status.AtProvider.ResourceOptionsAsClauses = roToAlter +func (c *external) updateResourceOptionsIfChanged(ctx context.Context, cr *v1alpha1.User, roToAlter []string, username string, host string) error { + if !checkResourceOptionsChanged(roToAlter) { + return nil } - // Handle auth plugin and/or password changes - if (checkUsePassword(cr) && passwordChanged) || checkAuthPluginChanged(cr) { - if err := c.executeAlterUserQuery(ctx, username, host, plugin, password); err != nil { - return managed.ExternalUpdate{}, err - } + resourceOptions := fmt.Sprintf(" WITH %s", strings.Join(roToAlter, " ")) + query := fmt.Sprintf( + "ALTER USER %s@%s%s", + mysql.QuoteValue(username), + mysql.QuoteValue(host), + resourceOptions, + ) + if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errUpdateUser}); err != nil { + return err } + cr.Status.AtProvider.ResourceOptionsAsClauses = roToAlter + return nil +} + +func (c *external) updateAuthIfChanged(ctx context.Context, cr *v1alpha1.User, passwordChanged bool, username string, host string, plugin string, password string) (managed.ExternalUpdate, error) { + needsPasswordUpdate := checkUsePassword(cr) && passwordChanged + needsAuthPluginUpdate := checkAuthPluginChanged(cr) - if checkUsePassword(cr) && passwordChanged { + if !needsPasswordUpdate && !needsAuthPluginUpdate { + return managed.ExternalUpdate{}, nil + } + + if err := c.executeAlterUserQuery(ctx, username, host, plugin, password); err != nil { + return managed.ExternalUpdate{}, err + } + + // Return connection details if password was updated + if needsPasswordUpdate { return managed.ExternalUpdate{ConnectionDetails: c.db.GetConnectionDetails(username, password)}, nil } @@ -406,23 +406,10 @@ func (c *external) applyAlterUserIfSomeFieldChanged(ctx context.Context, cr *v1a } func (c *external) executeAlterUserQuery(ctx context.Context, username string, host string, plugin string, pw string) error { - var identifiedClause string - - // When plugin is empty (nil or ""), use default auth: IDENTIFIED BY 'password' - // When plugin is specified, use: IDENTIFIED WITH plugin BY 'password' - // This ensures compatibility with both MySQL and MariaDB - if plugin == "" { - if pw != "" { - identifiedClause = fmt.Sprintf("IDENTIFIED BY %s", mysql.QuoteValue(pw)) - } else { - // No password and no plugin means nothing to update - return nil - } - } else { - identifiedClause = fmt.Sprintf("IDENTIFIED WITH %s", plugin) - if pw != "" { - identifiedClause += fmt.Sprintf(" BY %s", mysql.QuoteValue(pw)) - } + identifiedClause := buildIdentifiedClause(plugin, &pw) + if identifiedClause == "" { + // No password and no plugin means nothing to update + return nil } query := fmt.Sprintf("ALTER USER %s@%s %s", @@ -431,27 +418,26 @@ func (c *external) executeAlterUserQuery(ctx context.Context, username string, h identifiedClause, ) - if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errUpdateUser}); err != nil { - return err - } - - return nil + return mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errUpdateUser}) } -func getPassword(ctx context.Context, cr *v1alpha1.User, c *external) (string, bool, error) { - password := "" - passwordChanged := false - if checkUsePassword(cr) { - pw, pwdChanged, err := c.getPassword(ctx, cr) - if err != nil { - return pw, pwdChanged, err +// buildIdentifiedClause constructs the IDENTIFIED clause for CREATE/ALTER USER statements. +// When plugin is empty (nil or ""), uses default auth: IDENTIFIED BY 'password' +// When plugin is specified, uses: IDENTIFIED WITH plugin BY 'password' +// This ensures compatibility with both MySQL and MariaDB +func buildIdentifiedClause(plugin string, pw *string) string { + if plugin == "" { + if pw != nil && *pw != "" { + return fmt.Sprintf("IDENTIFIED BY %s", mysql.QuoteValue(*pw)) } - - password = pw - passwordChanged = pwdChanged + return "" } - return password, passwordChanged, nil + identifiedClause := fmt.Sprintf("IDENTIFIED WITH %s", plugin) + if pw != nil && *pw != "" { + identifiedClause += fmt.Sprintf(" BY %s", mysql.QuoteValue(*pw)) + } + return identifiedClause } func getResourceOptionsToAlter(cr *v1alpha1.User) ([]string, error) { From 2b7f14fcf4db275e1e729f353146381225f59f65 Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Mon, 20 Oct 2025 22:51:42 -0300 Subject: [PATCH 15/19] feat(mysql): add AuthPlugin and UsePassword support to namespaced User resource Signed-off-by: Alejandro Recalde --- apis/namespaced/mysql/v1alpha1/user_types.go | 15 ++ .../mysql/v1alpha1/zz_generated.deepcopy.go | 15 ++ .../mysql/user_with_auth_plugin.yaml | 51 ++++ .../crds/mysql.sql.m.crossplane.io_users.yaml | 16 ++ .../cluster/mysql/user/reconciler.go | 3 - .../namespaced/mysql/user/reconciler.go | 219 ++++++++++++++---- .../namespaced/mysql/user/reconciler_test.go | 136 ++++++++++- 7 files changed, 399 insertions(+), 56 deletions(-) create mode 100644 examples/namespaced/mysql/user_with_auth_plugin.yaml diff --git a/apis/namespaced/mysql/v1alpha1/user_types.go b/apis/namespaced/mysql/v1alpha1/user_types.go index d8a8a1c8..a3d8e467 100644 --- a/apis/namespaced/mysql/v1alpha1/user_types.go +++ b/apis/namespaced/mysql/v1alpha1/user_types.go @@ -42,6 +42,18 @@ type UserParameters struct { // +optional PasswordSecretRef *xpv1.LocalSecretKeySelector `json:"passwordSecretRef,omitempty"` + // AuthPlugin specifies the authentication plugin to use for the user. + // If not specified (nil or empty string), the database server's default authentication plugin is used. + // Common values include "mysql_native_password", "caching_sha2_password", "authentication_ldap_simple". + // See https://dev.mysql.com/doc/refman/8.0/en/authentication-plugins.html + // +optional + // +kubebuilder:validation:Pattern:=^([a-z]+_)+[a-z]+$ + AuthPlugin *string `json:"authPlugin,omitempty"` + + // UsePassword indicate whether the provided AuthPlugin requires setting a password, defaults to true + // +optional + UsePassword *bool `json:"usePassword,omitempty" default:"true"` + // ResourceOptions sets account specific resource limits. // See https://dev.mysql.com/doc/refman/8.0/en/user-resources.html // +optional @@ -73,6 +85,9 @@ type ResourceOptions struct { // A UserObservation represents the observed state of a MySQL user. type UserObservation struct { + // AuthPlugin is the authentication plugin currently configured for the user + AuthPlugin *string `json:"authPlugin,omitempty"` + // ResourceOptionsAsClauses represents the applied resource options ResourceOptionsAsClauses []string `json:"resourceOptionsAsClauses,omitempty"` } diff --git a/apis/namespaced/mysql/v1alpha1/zz_generated.deepcopy.go b/apis/namespaced/mysql/v1alpha1/zz_generated.deepcopy.go index 542e5a78..b290af09 100644 --- a/apis/namespaced/mysql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/namespaced/mysql/v1alpha1/zz_generated.deepcopy.go @@ -740,6 +740,11 @@ func (in *UserList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UserObservation) DeepCopyInto(out *UserObservation) { *out = *in + if in.AuthPlugin != nil { + in, out := &in.AuthPlugin, &out.AuthPlugin + *out = new(string) + **out = **in + } if in.ResourceOptionsAsClauses != nil { in, out := &in.ResourceOptionsAsClauses, &out.ResourceOptionsAsClauses *out = make([]string, len(*in)) @@ -765,6 +770,16 @@ func (in *UserParameters) DeepCopyInto(out *UserParameters) { *out = new(v1.LocalSecretKeySelector) **out = **in } + if in.AuthPlugin != nil { + in, out := &in.AuthPlugin, &out.AuthPlugin + *out = new(string) + **out = **in + } + if in.UsePassword != nil { + in, out := &in.UsePassword, &out.UsePassword + *out = new(bool) + **out = **in + } if in.ResourceOptions != nil { in, out := &in.ResourceOptions, &out.ResourceOptions *out = new(ResourceOptions) diff --git a/examples/namespaced/mysql/user_with_auth_plugin.yaml b/examples/namespaced/mysql/user_with_auth_plugin.yaml new file mode 100644 index 00000000..c89d4a9b --- /dev/null +++ b/examples/namespaced/mysql/user_with_auth_plugin.yaml @@ -0,0 +1,51 @@ +--- +# Example: Namespaced User with custom authentication plugin (e.g., LDAP) +# Some authentication plugins like authentication_ldap_simple don't require +# passwords, so usePassword is set to false +apiVersion: mysql.sql.m.crossplane.io/v1alpha1 +kind: User +metadata: + name: example-ldap-user + namespace: default +spec: + forProvider: + authPlugin: authentication_ldap_simple + usePassword: false # LDAP authentication doesn't use a MySQL password + providerConfigRef: + name: example +--- +# Example: Namespaced User with specific authentication plugin and password +# For plugins that require passwords like caching_sha2_password +apiVersion: mysql.sql.m.crossplane.io/v1alpha1 +kind: User +metadata: + name: example-caching-sha2-user + namespace: default +spec: + forProvider: + authPlugin: caching_sha2_password + passwordSecretRef: + name: example-pw + key: password + writeConnectionSecretToRef: + name: example-sha2-connection-secret + providerConfigRef: + name: example +--- +# Example: Namespaced User without authPlugin specified (uses database server default) +# This is the recommended approach for maximum compatibility +# across different MySQL/MariaDB versions +apiVersion: mysql.sql.m.crossplane.io/v1alpha1 +kind: User +metadata: + name: example-default-user + namespace: default +spec: + forProvider: + passwordSecretRef: + name: example-pw + key: password + writeConnectionSecretToRef: + name: example-default-connection-secret + providerConfigRef: + name: example diff --git a/package/crds/mysql.sql.m.crossplane.io_users.yaml b/package/crds/mysql.sql.m.crossplane.io_users.yaml index 3aab6808..bc6f08e1 100644 --- a/package/crds/mysql.sql.m.crossplane.io_users.yaml +++ b/package/crds/mysql.sql.m.crossplane.io_users.yaml @@ -57,6 +57,14 @@ spec: description: UserParameters define the desired state of a MySQL user instance. properties: + authPlugin: + description: |- + AuthPlugin specifies the authentication plugin to use for the user. + If not specified (nil or empty string), the database server's default authentication plugin is used. + Common values include "mysql_native_password", "caching_sha2_password", "authentication_ldap_simple". + See https://dev.mysql.com/doc/refman/8.0/en/authentication-plugins.html + pattern: ^([a-z]+_)+[a-z]+$ + type: string binlog: description: BinLog defines whether the create, delete, update operations of this user are propagated to replicas. Defaults @@ -98,6 +106,10 @@ spec: connections to the server by an account type: integer type: object + usePassword: + description: UsePassword indicate whether the provided AuthPlugin + requires setting a password, defaults to true + type: boolean type: object managementPolicies: default: @@ -164,6 +176,10 @@ spec: description: A UserObservation represents the observed state of a MySQL user. properties: + authPlugin: + description: AuthPlugin is the authentication plugin currently + configured for the user + type: string resourceOptionsAsClauses: description: ResourceOptionsAsClauses represents the applied resource options diff --git a/pkg/controller/cluster/mysql/user/reconciler.go b/pkg/controller/cluster/mysql/user/reconciler.go index 19e3be1f..8d2be10a 100644 --- a/pkg/controller/cluster/mysql/user/reconciler.go +++ b/pkg/controller/cluster/mysql/user/reconciler.go @@ -422,9 +422,6 @@ func (c *external) executeAlterUserQuery(ctx context.Context, username string, h } // buildIdentifiedClause constructs the IDENTIFIED clause for CREATE/ALTER USER statements. -// When plugin is empty (nil or ""), uses default auth: IDENTIFIED BY 'password' -// When plugin is specified, uses: IDENTIFIED WITH plugin BY 'password' -// This ensures compatibility with both MySQL and MariaDB func buildIdentifiedClause(plugin string, pw *string) string { if plugin == "" { if pw != nil && *pw != "" { diff --git a/pkg/controller/namespaced/mysql/user/reconciler.go b/pkg/controller/namespaced/mysql/user/reconciler.go index 843011ec..d0a8752b 100644 --- a/pkg/controller/namespaced/mysql/user/reconciler.go +++ b/pkg/controller/namespaced/mysql/user/reconciler.go @@ -199,6 +199,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex username, host := mysql.SplitUserHost(meta.GetExternalName(cr)) observed := &namespacedv1alpha1.UserParameters{ + AuthPlugin: new(string), ResourceOptions: &namespacedv1alpha1.ResourceOptions{}, } @@ -206,7 +207,8 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex "max_questions, " + "max_updates, " + "max_connections, " + - "max_user_connections " + + "max_user_connections, " + + "plugin " + "FROM mysql.user WHERE User = ? AND Host = ?" err := c.db.Scan(ctx, xsql.Query{ @@ -220,6 +222,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex &observed.ResourceOptions.MaxUpdatesPerHour, &observed.ResourceOptions.MaxConnectionsPerHour, &observed.ResourceOptions.MaxUserConnections, + observed.AuthPlugin, ) if xsql.IsNoRows(err) { return managed.ExternalObservation{ResourceExists: false}, nil @@ -234,6 +237,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex } cr.Status.AtProvider.ResourceOptionsAsClauses = resourceOptionsToClauses(observed.ResourceOptions) + cr.Status.AtProvider.AuthPlugin = observed.AuthPlugin cr.SetConditions(xpv1.Available()) @@ -252,20 +256,25 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext cr.SetConditions(xpv1.Creating()) username, host := mysql.SplitUserHost(meta.GetExternalName(cr)) - pw, _, err := c.getPassword(ctx, cr) - if err != nil { - return managed.ExternalCreation{}, err - } + ro := resourceOptionsToClauses(cr.Spec.ForProvider.ResourceOptions) - if pw == "" { - pw, err = password.Generate() + var pw *string + if checkUsePassword(cr) { + userPassword, _, err := c.getPassword(ctx, cr) if err != nil { return managed.ExternalCreation{}, err } + + if userPassword == "" { + userPassword, err = password.Generate() + if err != nil { + return managed.ExternalCreation{}, err + } + } + pw = &userPassword } - ro := resourceOptionsToClauses(cr.Spec.ForProvider.ResourceOptions) - if err := c.executeCreateUserQuery(ctx, username, host, ro, pw); err != nil { + if err := c.executeCreateUserQuery(ctx, username, host, cr.Spec.ForProvider.AuthPlugin, ro, pw); err != nil { return managed.ExternalCreation{}, err } @@ -273,30 +282,33 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext cr.Status.AtProvider.ResourceOptionsAsClauses = ro } - return managed.ExternalCreation{ - ConnectionDetails: c.db.GetConnectionDetails(username, pw), - }, nil + if pw != nil { + return managed.ExternalCreation{ + ConnectionDetails: c.db.GetConnectionDetails(username, *pw), + }, nil + } + + return managed.ExternalCreation{}, nil } -func (c *external) executeCreateUserQuery(ctx context.Context, username string, host string, resourceOptionsClauses []string, pw string) error { +func (c *external) executeCreateUserQuery(ctx context.Context, username string, host string, authPlugin *string, resourceOptionsClauses []string, pw *string) error { + plugin := defaultAuthPlugin(authPlugin) + identifiedClause := buildIdentifiedClause(plugin, pw) + resourceOptions := "" if len(resourceOptionsClauses) != 0 { resourceOptions = fmt.Sprintf(" WITH %s", strings.Join(resourceOptionsClauses, " ")) } query := fmt.Sprintf( - "CREATE USER %s@%s IDENTIFIED BY %s%s", + "CREATE USER %s@%s %s%s", mysql.QuoteValue(username), mysql.QuoteValue(host), - mysql.QuoteValue(pw), + identifiedClause, resourceOptions, ) - if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errCreateUser}); err != nil { - return err - } - - return nil + return mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errCreateUser}) } func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { @@ -306,57 +318,72 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext } username, host := mysql.SplitUserHost(meta.GetExternalName(cr)) + plugin := defaultAuthPlugin(cr.Spec.ForProvider.AuthPlugin) - ro := resourceOptionsToClauses(cr.Spec.ForProvider.ResourceOptions) - rochanged, err := changedResourceOptions(cr.Status.AtProvider.ResourceOptionsAsClauses, ro) + roToAlter, err := getResourceOptionsToAlter(cr) if err != nil { - return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser) + return managed.ExternalUpdate{}, err } - if len(rochanged) > 0 { - resourceOptions := fmt.Sprintf("WITH %s", strings.Join(ro, " ")) - - query := fmt.Sprintf( - "ALTER USER %s@%s %s", - mysql.QuoteValue(username), - mysql.QuoteValue(host), - resourceOptions, - ) - if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errUpdateUser}); err != nil { + password := "" + passwordChanged := false + if checkUsePassword(cr) { + password, passwordChanged, err = c.getPassword(ctx, cr) + if err != nil { return managed.ExternalUpdate{}, err } - - cr.Status.AtProvider.ResourceOptionsAsClauses = ro } - connectionDetails, err := c.UpdatePassword(ctx, cr, username, host) - if err != nil { + return c.applyUserChanges(ctx, cr, passwordChanged, roToAlter, username, host, plugin, password) +} + +func (c *external) applyUserChanges(ctx context.Context, cr *namespacedv1alpha1.User, passwordChanged bool, roToAlter []string, username string, host string, plugin string, password string) (managed.ExternalUpdate, error) { + // Handle resource options changes (separate ALTER USER statement) + if err := c.updateResourceOptionsIfChanged(ctx, cr, roToAlter, username, host); err != nil { return managed.ExternalUpdate{}, err } - if len(connectionDetails) > 0 { - return managed.ExternalUpdate{ConnectionDetails: connectionDetails}, nil + // Handle auth plugin and/or password changes (separate ALTER USER statement) + return c.updateAuthIfChanged(ctx, cr, passwordChanged, username, host, plugin, password) +} + +func (c *external) updateResourceOptionsIfChanged(ctx context.Context, cr *namespacedv1alpha1.User, roToAlter []string, username string, host string) error { + if !checkResourceOptionsChanged(roToAlter) { + return nil } - return managed.ExternalUpdate{}, nil + resourceOptions := fmt.Sprintf(" WITH %s", strings.Join(roToAlter, " ")) + query := fmt.Sprintf( + "ALTER USER %s@%s%s", + mysql.QuoteValue(username), + mysql.QuoteValue(host), + resourceOptions, + ) + if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errUpdateUser}); err != nil { + return err + } + cr.Status.AtProvider.ResourceOptionsAsClauses = roToAlter + return nil } -func (c *external) UpdatePassword(ctx context.Context, cr *namespacedv1alpha1.User, username, host string) (managed.ConnectionDetails, error) { - pw, pwchanged, err := c.getPassword(ctx, cr) - if err != nil { - return managed.ConnectionDetails{}, err +func (c *external) updateAuthIfChanged(ctx context.Context, cr *namespacedv1alpha1.User, passwordChanged bool, username string, host string, plugin string, password string) (managed.ExternalUpdate, error) { + needsPasswordUpdate := checkUsePassword(cr) && passwordChanged + needsAuthPluginUpdate := checkAuthPluginChanged(cr) + + if !needsPasswordUpdate && !needsAuthPluginUpdate { + return managed.ExternalUpdate{}, nil } - if pwchanged { - query := fmt.Sprintf("ALTER USER %s@%s IDENTIFIED BY %s", mysql.QuoteValue(username), mysql.QuoteValue(host), mysql.QuoteValue(pw)) - if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errUpdateUser}); err != nil { - return managed.ConnectionDetails{}, err - } + if err := c.executeAlterUserQuery(ctx, username, host, plugin, password); err != nil { + return managed.ExternalUpdate{}, err + } - return c.db.GetConnectionDetails(username, pw), nil + // Return connection details if password was updated + if needsPasswordUpdate { + return managed.ExternalUpdate{ConnectionDetails: c.db.GetConnectionDetails(username, password)}, nil } - return managed.ConnectionDetails{}, nil + return managed.ExternalUpdate{}, nil } func (c *external) Disconnect(ctx context.Context) error { @@ -381,7 +408,97 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalDelete{}, nil } +func checkUsePassword(cr *namespacedv1alpha1.User) bool { + if cr.Spec.ForProvider.UsePassword == nil { + return true + } + + return *cr.Spec.ForProvider.UsePassword +} + +// defaultAuthPlugin returns the authentication plugin to use. +// If the input is nil or an empty string, it returns "" to indicate using the server default. +// Otherwise, it returns the specified plugin name. +func defaultAuthPlugin(plugin *string) string { + if plugin == nil || *plugin == "" { + return "" + } + return *plugin +} + +func checkAuthPluginChanged(cr *namespacedv1alpha1.User) bool { + if cr.Status.AtProvider.AuthPlugin == nil { + return true + } + + if *cr.Status.AtProvider.AuthPlugin != defaultAuthPlugin(cr.Spec.ForProvider.AuthPlugin) { + return true + } + + return false +} + +func getResourceOptionsToAlter(cr *namespacedv1alpha1.User) ([]string, error) { + roToAlter := []string{} + + ro := resourceOptionsToClauses(cr.Spec.ForProvider.ResourceOptions) + roChanged, err := changedResourceOptions(cr.Status.AtProvider.ResourceOptionsAsClauses, ro) + if err != nil { + return roToAlter, errors.Wrap(err, errUpdateUser) + } + + if len(roChanged) > 0 { + roToAlter = ro + } + + return roToAlter, nil +} + +func checkResourceOptionsChanged(roToAlter []string) bool { + return len(roToAlter) > 0 +} + +func (c *external) executeAlterUserQuery(ctx context.Context, username string, host string, plugin string, pw string) error { + identifiedClause := buildIdentifiedClause(plugin, &pw) + if identifiedClause == "" { + // No password and no plugin means nothing to update + return nil + } + + query := fmt.Sprintf("ALTER USER %s@%s %s", + mysql.QuoteValue(username), + mysql.QuoteValue(host), + identifiedClause, + ) + + return mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errUpdateUser}) +} + +// buildIdentifiedClause constructs the IDENTIFIED clause for CREATE/ALTER USER statements. +func buildIdentifiedClause(plugin string, pw *string) string { + if plugin == "" { + if pw != nil && *pw != "" { + return fmt.Sprintf("IDENTIFIED BY %s", mysql.QuoteValue(*pw)) + } + return "" + } + + identifiedClause := fmt.Sprintf("IDENTIFIED WITH %s", plugin) + if pw != nil && *pw != "" { + identifiedClause += fmt.Sprintf(" BY %s", mysql.QuoteValue(*pw)) + } + return identifiedClause +} + func upToDate(observed *namespacedv1alpha1.UserParameters, desired *namespacedv1alpha1.UserParameters) bool { + // Check auth plugin + observedPlugin := defaultAuthPlugin(observed.AuthPlugin) + desiredPlugin := defaultAuthPlugin(desired.AuthPlugin) + if observedPlugin != desiredPlugin { + return false + } + + // Check resource options if desired.ResourceOptions == nil { // Return true if there are no desired ResourceOptions return true diff --git a/pkg/controller/namespaced/mysql/user/reconciler_test.go b/pkg/controller/namespaced/mysql/user/reconciler_test.go index 71630b02..369bd5ce 100644 --- a/pkg/controller/namespaced/mysql/user/reconciler_test.go +++ b/pkg/controller/namespaced/mysql/user/reconciler_test.go @@ -28,6 +28,7 @@ import ( "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/crossplane-runtime/v2/apis/common" @@ -307,7 +308,15 @@ func TestObserve(t *testing.T) { reason: "We should return no error if we can successfully select our user", fields: fields{ db: mockDB{ - MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { return nil }, + MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + // Set authPlugin to empty string (default) + if len(dest) >= 5 { + if pluginPtr, ok := dest[4].(*string); ok { + *pluginPtr = "" + } + } + return nil + }, }, }, args: args{ @@ -329,7 +338,15 @@ func TestObserve(t *testing.T) { reason: "We should return ResourceUpToDate=false if the password changed", fields: fields{ db: mockDB{ - MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { return nil }, + MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + // Set authPlugin to empty string (default) + if len(dest) >= 5 { + if pluginPtr, ok := dest[4].(*string); ok { + *pluginPtr = "" + } + } + return nil + }, }, kube: &test.MockClient{ MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { @@ -555,6 +572,34 @@ func TestCreate(t *testing.T) { }, }, }, + "UserWithAnAuthPluginThatNotRequiresPassword": { + reason: "A user that uses an authentication plugin that does not require password should not receive connection details and no error should happen", + comparePw: true, + fields: fields{ + db: &mockDB{ + MockExec: func(ctx context.Context, q xsql.Query) error { return nil }, + }, + }, + args: args{ + mg: &v1alpha1.User{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + meta.AnnotationKeyExternalName: "example", + }, + }, + Spec: v1alpha1.UserSpec{ + ForProvider: v1alpha1.UserParameters{ + AuthPlugin: ptr.To("authentication_ldap_simple"), + UsePassword: ptr.To(false), + }, + }, + }, + }, + want: want{ + err: nil, + c: managed.ExternalCreation{}, + }, + }, } for name, tc := range cases { @@ -700,6 +745,11 @@ func TestUpdate(t *testing.T) { }, }, }, + Status: v1alpha1.UserStatus{ + AtProvider: v1alpha1.UserObservation{ + AuthPlugin: ptr.To(""), + }, + }, }, kube: &test.MockClient{ MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { @@ -823,6 +873,7 @@ func TestUpdate(t *testing.T) { "MAX_CONNECTIONS_PER_HOUR 0", "MAX_USER_CONNECTIONS 0", }, + AuthPlugin: ptr.To(""), }, }, }, @@ -841,6 +892,87 @@ func TestUpdate(t *testing.T) { err: nil, }, }, + "UserWithAnAuthPluginThatNotRequiresPassword": { + reason: "A user that uses an authentication plugin that does not require password should not receive connection details and no error should happen", + fields: fields{ + db: &mockDB{ + MockExec: func(ctx context.Context, q xsql.Query) error { return nil }, + }, + }, + args: args{ + mg: &v1alpha1.User{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + meta.AnnotationKeyExternalName: "example", + }, + }, + Spec: v1alpha1.UserSpec{ + ForProvider: v1alpha1.UserParameters{ + AuthPlugin: ptr.To("authentication_ldap_simple"), + UsePassword: ptr.To(false), + }, + }, + Status: v1alpha1.UserStatus{ + AtProvider: v1alpha1.UserObservation{ + AuthPlugin: ptr.To("authentication_ldap_simple"), + }, + }, + }, + }, + want: want{ + err: nil, + c: managed.ExternalUpdate{}, + }, + }, + "UpdatedAuthPlugin": { + reason: "We should execute an SQL query if the auth plugin is not synced.", + fields: fields{ + db: &mockDB{ + MockExec: func(ctx context.Context, q xsql.Query) error { + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.User{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + meta.AnnotationKeyExternalName: "example", + }, + }, + Spec: v1alpha1.UserSpec{ + ForProvider: v1alpha1.UserParameters{ + PasswordSecretRef: &common.LocalSecretKeySelector{ + LocalSecretReference: common.LocalSecretReference{ + Name: "connection-secret", + }, + Key: xpv1.ResourceCredentialsSecretPasswordKey, + }, + AuthPlugin: ptr.To(""), + }, + }, + Status: v1alpha1.UserStatus{ + AtProvider: v1alpha1.UserObservation{ + AuthPlugin: ptr.To("authentication_ldap_simple"), + }, + }, + }, + kube: &test.MockClient{ + MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error { + secret := corev1.Secret{ + Data: map[string][]byte{}, + } + secret.Data[xpv1.ResourceCredentialsSecretPasswordKey] = []byte("samesame") + secret.DeepCopyInto(obj.(*corev1.Secret)) + return nil + }, + }, + }, + want: want{ + err: nil, + c: managed.ExternalUpdate{}, + }, + }, } for name, tc := range cases { From 087fb5e45a2ba4f074bf20b1fa9a3c41ae5c880a Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Thu, 22 Jan 2026 00:21:42 -0300 Subject: [PATCH 16/19] fix(mysql): update authentication plugin pattern for user spec to allow more flexibility Signed-off-by: Alejandro Recalde --- package/crds/mysql.sql.crossplane.io_users.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/crds/mysql.sql.crossplane.io_users.yaml b/package/crds/mysql.sql.crossplane.io_users.yaml index 532979f5..070f30dc 100644 --- a/package/crds/mysql.sql.crossplane.io_users.yaml +++ b/package/crds/mysql.sql.crossplane.io_users.yaml @@ -77,7 +77,7 @@ spec: If not specified (nil or empty string), the database server's default authentication plugin is used. This allows compatibility with different MySQL/MariaDB versions and their default authentication methods. Common plugins: caching_sha2_password (MySQL 8.0+), mysql_native_password, authentication_ldap_simple, etc. - pattern: ^([a-z]+_)+[a-z]+$ + pattern: ^[a-z][a-z0-9_]*$ type: string binlog: description: BinLog defines whether the create, delete, update From dafe9b7e3ad04a45aba8a0d5b4b4b3516306d277 Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Sat, 24 Jan 2026 12:54:27 -0300 Subject: [PATCH 17/19] fix(mysql): update authentication plugin pattern for user spec to allow more flexibility Signed-off-by: Alejandro Recalde --- package/crds/mysql.sql.m.crossplane.io_users.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/crds/mysql.sql.m.crossplane.io_users.yaml b/package/crds/mysql.sql.m.crossplane.io_users.yaml index bc6f08e1..68d3bdba 100644 --- a/package/crds/mysql.sql.m.crossplane.io_users.yaml +++ b/package/crds/mysql.sql.m.crossplane.io_users.yaml @@ -63,7 +63,7 @@ spec: If not specified (nil or empty string), the database server's default authentication plugin is used. Common values include "mysql_native_password", "caching_sha2_password", "authentication_ldap_simple". See https://dev.mysql.com/doc/refman/8.0/en/authentication-plugins.html - pattern: ^([a-z]+_)+[a-z]+$ + pattern: ^[a-z][a-z0-9_]*$ type: string binlog: description: BinLog defines whether the create, delete, update From af59583ec75bc55d1b047601bdafa7a46571a605 Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Sat, 24 Jan 2026 15:47:47 -0300 Subject: [PATCH 18/19] feat(mysql): implement UpdatePassword method to handle password and auth plugin updates Signed-off-by: Alejandro Recalde --- .../cluster/mysql/user/reconciler.go | 62 ++++++++++++++----- .../namespaced/mysql/user/reconciler.go | 40 ++++++------ 2 files changed, 64 insertions(+), 38 deletions(-) diff --git a/pkg/controller/cluster/mysql/user/reconciler.go b/pkg/controller/cluster/mysql/user/reconciler.go index 4427c1f9..02f00ff9 100644 --- a/pkg/controller/cluster/mysql/user/reconciler.go +++ b/pkg/controller/cluster/mysql/user/reconciler.go @@ -333,6 +333,52 @@ func (c *external) Update(ctx context.Context, mg *v1alpha1.User) (managed.Exter return managed.ExternalUpdate{ConnectionDetails: connectionDetails}, nil } +// UpdatePassword updates the password and/or auth plugin for a user if either has changed +func (c *external) UpdatePassword(ctx context.Context, mg *v1alpha1.User, username string, host string) (managed.ConnectionDetails, error) { + pw, pwChanged, err := c.getPassword(ctx, mg) + if err != nil { + return nil, err + } + + pluginChanged := false + desiredPlugin := defaultAuthPlugin(mg.Spec.ForProvider.AuthPlugin) + if mg.Status.AtProvider.AuthPlugin != nil { + observedPlugin := defaultAuthPlugin(mg.Status.AtProvider.AuthPlugin) + pluginChanged = desiredPlugin != observedPlugin + } + + if !pwChanged && !pluginChanged { + return nil, nil + } + + if err := c.executeAlterUserQuery(ctx, username, host, desiredPlugin, pw); err != nil { + return nil, err + } + + if pwChanged { + return c.db.GetConnectionDetails(username, pw), nil + } + + return nil, nil +} + +// executeAlterUserQuery executes an ALTER USER statement to update password/plugin +func (c *external) executeAlterUserQuery(ctx context.Context, username string, host string, plugin string, pw string) error { + identifiedClause := buildIdentifiedClause(plugin, &pw) + if identifiedClause == "" { + // No password and no plugin means nothing to update + return nil + } + + query := fmt.Sprintf("ALTER USER %s@%s %s", + mysql.QuoteValue(username), + mysql.QuoteValue(host), + identifiedClause, + ) + + return mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errUpdateUser}) +} + // buildIdentifiedClause constructs the IDENTIFIED clause for CREATE/ALTER USER statements. func buildIdentifiedClause(plugin string, pw *string) string { if plugin == "" { @@ -357,22 +403,6 @@ func checkUsePassword(mg *v1alpha1.User) bool { return *mg.Spec.ForProvider.UsePassword } -func checkResourceOptionsChanged(roToAlter []string) bool { - return len(roToAlter) > 0 -} - -func checkAuthPluginChanged(mg *v1alpha1.User) bool { - if mg.Status.AtProvider.AuthPlugin == nil { - return true - } - - if *mg.Status.AtProvider.AuthPlugin != defaultAuthPlugin(mg.Spec.ForProvider.AuthPlugin) { - return true - } - - return false -} - func defaultAuthPlugin(authPlugin *string) string { // nil or empty string means use the default plugin (let MySQL/MariaDB decide) // This avoids hardcoding mysql_native_password which is deprecated in MySQL 8.0.34+ diff --git a/pkg/controller/namespaced/mysql/user/reconciler.go b/pkg/controller/namespaced/mysql/user/reconciler.go index 1f326df0..15dccda9 100644 --- a/pkg/controller/namespaced/mysql/user/reconciler.go +++ b/pkg/controller/namespaced/mysql/user/reconciler.go @@ -285,7 +285,6 @@ func (c *external) executeCreateUserQuery(ctx context.Context, username string, func (c *external) Update(ctx context.Context, mg *namespacedv1alpha1.User) (managed.ExternalUpdate, error) { username, host := mysql.SplitUserHost(meta.GetExternalName(mg)) - plugin := defaultAuthPlugin(mg.Spec.ForProvider.AuthPlugin) ro := resourceOptionsToClauses(mg.Spec.ForProvider.ResourceOptions) rochanged, err := changedResourceOptions(mg.Status.AtProvider.ResourceOptionsAsClauses, ro) @@ -352,36 +351,33 @@ func defaultAuthPlugin(plugin *string) string { return *plugin } -func checkAuthPluginChanged(mg *namespacedv1alpha1.User) bool { - if mg.Status.AtProvider.AuthPlugin == nil { - return true +// UpdatePassword updates the password and/or auth plugin for a user if either has changed +func (c *external) UpdatePassword(ctx context.Context, mg *namespacedv1alpha1.User, username string, host string) (managed.ConnectionDetails, error) { + pw, pwChanged, err := c.getPassword(ctx, mg) + if err != nil { + return nil, err } - if *mg.Status.AtProvider.AuthPlugin != defaultAuthPlugin(mg.Spec.ForProvider.AuthPlugin) { - return true + pluginChanged := false + desiredPlugin := defaultAuthPlugin(mg.Spec.ForProvider.AuthPlugin) + if mg.Status.AtProvider.AuthPlugin != nil { + observedPlugin := defaultAuthPlugin(mg.Status.AtProvider.AuthPlugin) + pluginChanged = desiredPlugin != observedPlugin } - return false -} - -func getResourceOptionsToAlter(mg *namespacedv1alpha1.User) ([]string, error) { - roToAlter := []string{} - - ro := resourceOptionsToClauses(mg.Spec.ForProvider.ResourceOptions) - roChanged, err := changedResourceOptions(mg.Status.AtProvider.ResourceOptionsAsClauses, ro) - if err != nil { - return roToAlter, errors.Wrap(err, errUpdateUser) + if !pwChanged && !pluginChanged { + return nil, nil } - if len(roChanged) > 0 { - roToAlter = ro + if err := c.executeAlterUserQuery(ctx, username, host, desiredPlugin, pw); err != nil { + return nil, err } - return roToAlter, nil -} + if pwChanged { + return c.db.GetConnectionDetails(username, pw), nil + } -func checkResourceOptionsChanged(roToAlter []string) bool { - return len(roToAlter) > 0 + return nil, nil } func (c *external) executeAlterUserQuery(ctx context.Context, username string, host string, plugin string, pw string) error { From 2d28dfcce38814fd01fa44c58ffa82aea1c91e44 Mon Sep 17 00:00:00 2001 From: Alejandro Recalde Date: Sat, 24 Jan 2026 16:46:12 -0300 Subject: [PATCH 19/19] fix(mysql): update authentication plugin pattern to allow underscores in user names Signed-off-by: Alejandro Recalde --- package/crds/mysql.sql.crossplane.io_users.yaml | 2 +- package/crds/mysql.sql.m.crossplane.io_users.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package/crds/mysql.sql.crossplane.io_users.yaml b/package/crds/mysql.sql.crossplane.io_users.yaml index 070f30dc..532979f5 100644 --- a/package/crds/mysql.sql.crossplane.io_users.yaml +++ b/package/crds/mysql.sql.crossplane.io_users.yaml @@ -77,7 +77,7 @@ spec: If not specified (nil or empty string), the database server's default authentication plugin is used. This allows compatibility with different MySQL/MariaDB versions and their default authentication methods. Common plugins: caching_sha2_password (MySQL 8.0+), mysql_native_password, authentication_ldap_simple, etc. - pattern: ^[a-z][a-z0-9_]*$ + pattern: ^([a-z]+_)+[a-z]+$ type: string binlog: description: BinLog defines whether the create, delete, update diff --git a/package/crds/mysql.sql.m.crossplane.io_users.yaml b/package/crds/mysql.sql.m.crossplane.io_users.yaml index 68d3bdba..bc6f08e1 100644 --- a/package/crds/mysql.sql.m.crossplane.io_users.yaml +++ b/package/crds/mysql.sql.m.crossplane.io_users.yaml @@ -63,7 +63,7 @@ spec: If not specified (nil or empty string), the database server's default authentication plugin is used. Common values include "mysql_native_password", "caching_sha2_password", "authentication_ldap_simple". See https://dev.mysql.com/doc/refman/8.0/en/authentication-plugins.html - pattern: ^[a-z][a-z0-9_]*$ + pattern: ^([a-z]+_)+[a-z]+$ type: string binlog: description: BinLog defines whether the create, delete, update