Skip to content
12 changes: 12 additions & 0 deletions apis/mysql/v1alpha1/user_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -70,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
Expand Down
15 changes: 15 additions & 0 deletions apis/mysql/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions package/crds/mysql.sql.crossplane.io_users.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -281,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
Expand Down
218 changes: 161 additions & 57 deletions pkg/controller/mysql/user/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,16 @@ 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{},
}

query := "SELECT " +
"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{
Expand All @@ -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
Expand All @@ -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())

Expand All @@ -240,48 +244,74 @@ 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
plugin := defaultAuthPlugin(cr.Spec.ForProvider.AuthPlugin)

ro := resourceOptionsToClauses(cr.Spec.ForProvider.ResourceOptions)
if len(ro) != 0 {
cr.Status.AtProvider.ResourceOptionsAsClauses = ro
}
if pw == "" {
pw, err = password.Generate()

if checkUsePassword(cr) {
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, ro, &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, 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, 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 BY %s%s",
"CREATE USER %s@%s IDENTIFIED WITH %s%s%s",
mysql.QuoteValue(username),
mysql.QuoteValue(host),
mysql.QuoteValue(pw),
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) {
Expand All @@ -291,58 +321,132 @@ 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)
plugin := defaultAuthPlugin(cr.Spec.ForProvider.AuthPlugin)

roToAlter, err := getResourceOptionsToAlter(cr)
if err != nil {
return managed.ExternalUpdate{}, err
}

ro := resourceOptionsToClauses(cr.Spec.ForProvider.ResourceOptions)
rochanged, err := changedResourceOptions(cr.Status.AtProvider.ResourceOptionsAsClauses, ro)
password, passwordChanged, err := getPassword(ctx, cr, c)
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, " "))
return c.applyAlterUserIfSomeFieldChanged(ctx, cr, passwordChanged, roToAlter, username, host, plugin, password)
}

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)
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
}
if err := c.db.Exec(ctx, xsql.Query{
String: "FLUSH PRIVILEGES",
}); err != nil {
return managed.ExternalUpdate{}, errors.Wrap(err, errFlushPriv)
}

if checkUsePassword(cr) && passwordChanged {
return managed.ExternalUpdate{ConnectionDetails: c.db.GetConnectionDetails(username, password)}, nil
}

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 pw, pwdChanged, err
}

password = pw
passwordChanged = pwdChanged
}

return password, passwordChanged, 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
}

if pwchanged {
query := fmt.Sprintf("ALTER USER %s@%s IDENTIFIED BY %s", mysql.QuoteValue(username), mysql.QuoteValue(host), mysql.QuoteValue(pw))
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 roToAlter, nil
}

return managed.ExternalUpdate{
ConnectionDetails: c.db.GetConnectionDetails(username, pw),
}, nil
func checkUsePassword(cr *v1alpha1.User) bool {
if cr.Spec.ForProvider.UsePassword == nil {
return true
}
return managed.ExternalUpdate{}, nil

return *cr.Spec.ForProvider.UsePassword
}

func checkResourceOptionsChanged(roToAlter []string) bool {
return len(roToAlter) > 0
}

func checkAuthPluginChanged(cr *v1alpha1.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 (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))
}

resourceOptions := ""
if len(resourceOptionsClauses) != 0 {
resourceOptions = fmt.Sprintf(" WITH %s", strings.Join(resourceOptionsClauses, " "))
}

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 {
if authPlugin == nil {
return "mysql_native_password"
}

return *authPlugin
}

func (c *external) Delete(ctx context.Context, mg resource.Managed) error {
Expand Down
Loading