diff --git a/api/v1alpha1/fields.go b/api/v1alpha1/fields.go index 9cd8e08..bc800a0 100644 --- a/api/v1alpha1/fields.go +++ b/api/v1alpha1/fields.go @@ -1,5 +1,7 @@ package v1alpha1 +type OwnerID string + type SqlCredentials struct { // Username for the sql user Username string `json:"username,omitempty"` diff --git a/api/v1alpha1/sqldatabase_types.go b/api/v1alpha1/sqldatabase_types.go index d57d610..12ab2de 100644 --- a/api/v1alpha1/sqldatabase_types.go +++ b/api/v1alpha1/sqldatabase_types.go @@ -46,6 +46,9 @@ type SqlDatabaseStatus struct { // Timestamp when the user was last updated/checked. LastModifiedTimestamp *metav1.Time `json:"lastModifiedTimestamp,omitempty"` + + // String used to identify owership + OwnerID OwnerID `json:"ownerID,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1alpha1/sqlgrants_types.go b/api/v1alpha1/sqlgrants_types.go index a5f590e..d6f42ea 100644 --- a/api/v1alpha1/sqlgrants_types.go +++ b/api/v1alpha1/sqlgrants_types.go @@ -51,6 +51,9 @@ type SqlGrantStatus struct { LastModifiedTimestamp *metav1.Time `json:"lastModifiedTimestamp,omitempty"` CurrentGrants []string `json:"currentGrants,omitempty"` + + // String used to identify owership + OwnerID OwnerID `json:"ownerID,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1alpha1/sqluser_types.go b/api/v1alpha1/sqluser_types.go index dbbfd99..907011c 100644 --- a/api/v1alpha1/sqluser_types.go +++ b/api/v1alpha1/sqluser_types.go @@ -46,6 +46,9 @@ type SqlUserStatus struct { // Timestamp when the user was last updated/checked. LastModifiedTimestamp *metav1.Time `json:"lastModifiedTimestamp,omitempty"` + + // String used to identify owership + OwnerID OwnerID `json:"ownerID,omitempty"` } //+kubebuilder:object:root=true diff --git a/charts/sql-operator/crds/crds.yaml b/charts/sql-operator/crds/crds.yaml index 11c5211..3b3ee02 100644 --- a/charts/sql-operator/crds/crds.yaml +++ b/charts/sql-operator/crds/crds.yaml @@ -90,6 +90,9 @@ spec: description: Timestamp when the user was last updated/checked. format: date-time type: string + ownerID: + description: String used to identify owership + type: string required: - created type: object @@ -204,6 +207,9 @@ spec: description: Timestamp when the user was last updated/checked. format: date-time type: string + ownerID: + description: String used to identify owership + type: string required: - created type: object @@ -363,6 +369,9 @@ spec: description: Timestamp when the user was last updated/checked. format: date-time type: string + ownerID: + description: String used to identify owership + type: string required: - created type: object diff --git a/config/crd/bases/stenic.io_sqldatabases.yaml b/config/crd/bases/stenic.io_sqldatabases.yaml index 91fca25..94078d7 100644 --- a/config/crd/bases/stenic.io_sqldatabases.yaml +++ b/config/crd/bases/stenic.io_sqldatabases.yaml @@ -77,6 +77,9 @@ spec: description: Timestamp when the user was last updated/checked. format: date-time type: string + ownerID: + description: String used to identify owership + type: string required: - created type: object diff --git a/config/crd/bases/stenic.io_sqlgrants.yaml b/config/crd/bases/stenic.io_sqlgrants.yaml index 736c30b..d551365 100644 --- a/config/crd/bases/stenic.io_sqlgrants.yaml +++ b/config/crd/bases/stenic.io_sqlgrants.yaml @@ -98,6 +98,9 @@ spec: description: Timestamp when the user was last updated/checked. format: date-time type: string + ownerID: + description: String used to identify owership + type: string required: - created type: object diff --git a/config/crd/bases/stenic.io_sqlusers.yaml b/config/crd/bases/stenic.io_sqlusers.yaml index acf7055..6ccea63 100644 --- a/config/crd/bases/stenic.io_sqlusers.yaml +++ b/config/crd/bases/stenic.io_sqlusers.yaml @@ -83,6 +83,9 @@ spec: description: Timestamp when the user was last updated/checked. format: date-time type: string + ownerID: + description: String used to identify owership + type: string required: - created type: object diff --git a/controllers/owner.go b/controllers/owner.go new file mode 100644 index 0000000..6f2e49c --- /dev/null +++ b/controllers/owner.go @@ -0,0 +1,36 @@ +package controllers + +import ( + "context" + "fmt" + + steniciov1alpha1 "github.com/stenic/sql-operator/api/v1alpha1" + "github.com/stenic/sql-operator/drivers" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func checkOwner(ctx context.Context, driver drivers.Driver, ownerShipData drivers.OwnerShipData, obj client.Object) (steniciov1alpha1.OwnerID, error) { + ownerState, err := driver.GetOwnerState(ctx, ownerShipData) + switch ownerState { + case drivers.NonExisting: + ownerShipData.OwnerID = steniciov1alpha1.OwnerID(obj.GetUID()) + if err := driver.SetOwnerState(ctx, ownerShipData); err != nil { + return "", fmt.Errorf("failed to claim ownership - %s", err.Error()) + } + return ownerShipData.OwnerID, nil + case drivers.NotOwner: + // We are not managing this resource) + return "", fmt.Errorf("resource already exists and will not managed by this sqlOperator") + case drivers.IsOwner: + // All good, proceed + break + default: + return "", fmt.Errorf("unknown ownerState '%s'", ownerState) + } + + return "", err +} + +func cleanupOwner(ctx context.Context, driver drivers.Driver, ownerShipData drivers.OwnerShipData) error { + return driver.DeleteOwnerState(ctx, ownerShipData) +} diff --git a/controllers/sqldatabase_controller.go b/controllers/sqldatabase_controller.go index 83970ce..5048307 100644 --- a/controllers/sqldatabase_controller.go +++ b/controllers/sqldatabase_controller.go @@ -85,6 +85,18 @@ func (r *SqlDatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{}, err } + // Provide ownership data + err = driver.SetOwnershipData(ctx, drivers.OwnerShipData{ + Type: drivers.OwnerShipTypeDatabase, + Name: database.Spec.DatabaseName, + Resource: req.NamespacedName.String(), + OwnerID: database.Status.OwnerID, + }) + if err != nil { + r.Recorder.Event(&host, "Warning", "Error", err.Error()) + return ctrl.Result{RequeueAfter: r.RefreshRate * 2}, err + } + scheduledResult := ctrl.Result{RequeueAfter: r.RefreshRate} finalizerName := "stenic.io/sqldatabase-deletion" @@ -113,7 +125,7 @@ func (r *SqlDatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) if len(children.Items) > 0 { err := fmt.Errorf( "%s - [%s/%s] ...", - "Can't delete, found other referencing this object", + "can't delete, found other referencing this object", children.Items[0].Namespace, children.Items[0].Name, ) @@ -122,15 +134,22 @@ func (r *SqlDatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) // might have been faster than referenced object, reschedule. return scheduledResult, err } + r.Recorder.Event(&database, "Normal", "Delete", "Validated no child objects") - if database.Spec.CleanupPolicy == steniciov1alpha1.CleanupPolicyDelete { - // delete the user - if err = driver.DeleteDatabase(ctx, database); err != nil { - r.Recorder.Event(&database, "Warning", "Error", err.Error()) - sqlOperatorActionsFailures.With(promLabels).Inc() - return ctrl.Result{}, err - } + // Cleanup ref + if err := driver.DeleteOwnerState(ctx); err != nil { + log.Error(err, "unable to cleanup ownership") + return ctrl.Result{}, err } + r.Recorder.Event(&database, "Normal", "Delete", "Removed owner references") + + // delete the user + if err = driver.DeleteDatabase(ctx, database); err != nil { + r.Recorder.Event(&database, "Warning", "Error", err.Error()) + sqlOperatorActionsFailures.With(promLabels).Inc() + return ctrl.Result{}, err + } + r.Recorder.Event(&database, "Normal", "Delete", "Deleted mysql object") // remove our finalizer from the list and update it. controllerutil.RemoveFinalizer(&database, finalizerName) @@ -143,20 +162,14 @@ func (r *SqlDatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{}, nil } - // Deduplicate control loop - if database.Status.LastModifiedTimestamp != nil && time.Since(database.Status.LastModifiedTimestamp.Time) < r.RefreshRate { + if driver.Noop { + r.Recorder.Event(&database, "Normal", "Noop", "Determined object is not owned") return ctrl.Result{}, nil } - if !database.Status.Created { - database.Status.Created = true - database.Status.CreationTimestamp = &metav1.Time{Time: time.Now()} - } - database.Status.LastModifiedTimestamp = &metav1.Time{Time: time.Now()} - - if err := r.Status().Update(ctx, &database); err != nil { - log.Error(err, "unable to update SqlDatabase status") - return ctrl.Result{}, err + // Deduplicate control loop + if database.Status.LastModifiedTimestamp != nil && time.Since(database.Status.LastModifiedTimestamp.Time) < r.RefreshRate { + return ctrl.Result{}, nil } count, err := driver.UpsertDatabase(ctx, database) @@ -169,6 +182,18 @@ func (r *SqlDatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) if count > 0 { r.Recorder.Event(&database, "Normal", "Changed", fmt.Sprintf("%d queries executed", count)) sqlOperatorQueries.With(promLabels).Add(float64(count)) + + if !database.Status.Created { + database.Status.Created = true + database.Status.CreationTimestamp = &metav1.Time{Time: time.Now()} + } + database.Status.LastModifiedTimestamp = &metav1.Time{Time: time.Now()} + database.Status.OwnerID = driver.GetOwnerID() + + if err := r.Status().Update(ctx, &database); err != nil { + log.Error(err, "unable to update SqlDatabase status") + return ctrl.Result{}, err + } } return scheduledResult, nil diff --git a/controllers/sqlgrants_controller.go b/controllers/sqlgrants_controller.go index f7a68d4..70f2c94 100644 --- a/controllers/sqlgrants_controller.go +++ b/controllers/sqlgrants_controller.go @@ -105,6 +105,18 @@ func (r *SqlGrantReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c return ctrl.Result{}, err } + // Provide ownership data + err = driver.SetOwnershipData(ctx, drivers.OwnerShipData{ + Type: drivers.OwnerShipTypeGrant, + Name: grants.Name, + Resource: req.NamespacedName.String(), + OwnerID: grants.Status.OwnerID, + }) + if err != nil { + r.Recorder.Event(&host, "Warning", "Error", err.Error()) + return ctrl.Result{RequeueAfter: r.RefreshRate * 2}, err + } + finalizerName := "stenic.io/sqlgrants-deletion" // examine DeletionTimestamp to determine if object is under deletion if grants.ObjectMeta.DeletionTimestamp.IsZero() { @@ -122,14 +134,20 @@ func (r *SqlGrantReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c if controllerutil.ContainsFinalizer(&grants, finalizerName) { // our finalizer is present, so lets handle any external dependency - if grants.Spec.CleanupPolicy == steniciov1alpha1.CleanupPolicyDelete { - // delete the user - if err = driver.DeleteGrants(ctx, grants, user, database); err != nil { - r.Recorder.Event(&grants, "Warning", "Error", err.Error()) - sqlOperatorActionsFailures.With(promLabels).Inc() - return ctrl.Result{}, err - } + // Cleanup ref + if err := driver.DeleteOwnerState(ctx); err != nil { + log.Error(err, "unable to cleanup ownership") + return ctrl.Result{}, err + } + r.Recorder.Event(&grants, "Normal", "Delete", "Removed owner references") + + // delete the grant + if err = driver.DeleteGrants(ctx, grants, user, database); err != nil { + r.Recorder.Event(&grants, "Warning", "Error", err.Error()) + sqlOperatorActionsFailures.With(promLabels).Inc() + return ctrl.Result{}, err } + r.Recorder.Event(&grants, "Normal", "Delete", "Deleted mysql object") // remove our finalizer from the list and update it. controllerutil.RemoveFinalizer(&grants, finalizerName) @@ -142,6 +160,11 @@ func (r *SqlGrantReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c return ctrl.Result{}, nil } + if driver.Noop { + r.Recorder.Event(&grants, "Normal", "Noop", "Determined object is not owned") + return ctrl.Result{}, nil + } + // Deduplicate control loop if grants.Status.LastModifiedTimestamp != nil && time.Since(grants.Status.LastModifiedTimestamp.Time) < r.RefreshRate { return ctrl.Result{}, nil @@ -151,12 +174,6 @@ func (r *SqlGrantReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c grants.Status.CurrentGrants = []string{} } - if !grants.Status.Created { - grants.Status.Created = true - grants.Status.CreationTimestamp = &metav1.Time{Time: time.Now()} - } - grants.Status.LastModifiedTimestamp = &metav1.Time{Time: time.Now()} - if err := r.Status().Update(ctx, &grants); err != nil { log.Error(err, "unable to update SqlGrant status") return ctrl.Result{}, err @@ -172,12 +189,18 @@ func (r *SqlGrantReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c if count > 0 { r.Recorder.Event(&grants, "Normal", "Changed", fmt.Sprintf("%d queries executed", count)) sqlOperatorQueries.With(promLabels).Add(float64(count)) - } - grants.Status.CurrentGrants = grants.Spec.Grants - if err := r.Status().Update(ctx, &grants); err != nil { - log.Error(err, "unable to update SqlGrant status") - return ctrl.Result{}, err + if !grants.Status.Created { + grants.Status.Created = true + grants.Status.CreationTimestamp = &metav1.Time{Time: time.Now()} + } + grants.Status.LastModifiedTimestamp = &metav1.Time{Time: time.Now()} + grants.Status.CurrentGrants = grants.Spec.Grants + + if err := r.Status().Update(ctx, &grants); err != nil { + log.Error(err, "unable to update SqlGrant status") + return ctrl.Result{}, err + } } return scheduledResult, nil diff --git a/controllers/sqlhost_controller.go b/controllers/sqlhost_controller.go index 4a31155..2ad8d50 100644 --- a/controllers/sqlhost_controller.go +++ b/controllers/sqlhost_controller.go @@ -30,6 +30,7 @@ import ( "github.com/prometheus/client_golang/prometheus" steniciov1alpha1 "github.com/stenic/sql-operator/api/v1alpha1" + "github.com/stenic/sql-operator/drivers" ) // SqlHostReconciler reconciles a SqlHost object @@ -67,6 +68,16 @@ func (r *SqlHostReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, client.IgnoreNotFound(err) } + driver, err := drivers.GetDriver(host) + if err != nil { + return ctrl.Result{}, err + } + + if err := driver.InitOwnerSchema(ctx); err != nil { + r.Recorder.Event(&host, "Warning", "Error", err.Error()) + return ctrl.Result{RequeueAfter: r.RefreshRate * 10}, err + } + scheduledResult := ctrl.Result{RequeueAfter: r.RefreshRate} finalizerName := "stenic.io/sqlhost-deletion" diff --git a/controllers/sqluser_controller.go b/controllers/sqluser_controller.go index 87ba3d7..66d53c3 100644 --- a/controllers/sqluser_controller.go +++ b/controllers/sqluser_controller.go @@ -79,6 +79,19 @@ func (r *SqlUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct if err != nil { return ctrl.Result{}, err } + + // Provide ownership data + err = driver.SetOwnershipData(ctx, drivers.OwnerShipData{ + Type: drivers.OwnerShipTypeUser, + Name: user.Spec.Credentials.Username, + Resource: req.NamespacedName.String(), + OwnerID: user.Status.OwnerID, + }) + if err != nil { + r.Recorder.Event(&host, "Warning", "Error", err.Error()) + return ctrl.Result{RequeueAfter: r.RefreshRate * 2}, err + } + scheduledResult := ctrl.Result{RequeueAfter: r.RefreshRate} finalizerName := "stenic.io/sqluser-deletion" @@ -107,7 +120,7 @@ func (r *SqlUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct if len(children.Items) > 0 { err := fmt.Errorf( "%s - [%s/%s] ...", - "Can't delete, found other referencing this object", + "can't delete, found other referencing this object", children.Items[0].Namespace, children.Items[0].Name, ) @@ -116,15 +129,22 @@ func (r *SqlUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct // might have been faster than referenced object, reschedule. return scheduledResult, err } + r.Recorder.Event(&user, "Normal", "Delete", "Validated no child objects") - if user.Spec.CleanupPolicy == steniciov1alpha1.CleanupPolicyDelete { - // delete the user - if err = driver.DeleteUser(ctx, user); err != nil { - r.Recorder.Event(&user, "Warning", "Error", err.Error()) - sqlOperatorActionsFailures.With(promLabels).Inc() - return ctrl.Result{}, err - } + // Cleanup ref + if err := driver.DeleteOwnerState(ctx); err != nil { + log.Error(err, "unable to cleanup ownership") + return ctrl.Result{}, err } + r.Recorder.Event(&user, "Normal", "Delete", "Removed owner references") + + // delete the user + if err = driver.DeleteUser(ctx, user); err != nil { + r.Recorder.Event(&user, "Warning", "Error", err.Error()) + sqlOperatorActionsFailures.With(promLabels).Inc() + return ctrl.Result{}, err + } + r.Recorder.Event(&user, "Normal", "Delete", "Deleted mysql object") // remove our finalizer from the list and update it. controllerutil.RemoveFinalizer(&user, finalizerName) @@ -137,21 +157,16 @@ func (r *SqlUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, nil } - // Deduplicate control loop - if user.Status.LastModifiedTimestamp != nil && time.Since(user.Status.LastModifiedTimestamp.Time) < r.RefreshRate { + if driver.Noop { + r.Recorder.Event(&user, "Normal", "Noop", "Determined object is not owned") return ctrl.Result{}, nil } - if !user.Status.Created { - user.Status.Created = true - user.Status.CreationTimestamp = &metav1.Time{Time: time.Now()} + // Deduplicate control loop + if user.Status.LastModifiedTimestamp != nil && time.Since(user.Status.LastModifiedTimestamp.Time) < r.RefreshRate { + return ctrl.Result{}, nil } - user.Status.LastModifiedTimestamp = &metav1.Time{Time: time.Now()} - if err := r.Status().Update(ctx, &user); err != nil { - log.Error(err, "unable to update SqlUser status") - return ctrl.Result{}, err - } count, err := driver.UpsertUser(ctx, user) if err != nil { log.Error(err, "failed to create SqlUser") @@ -162,6 +177,18 @@ func (r *SqlUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct if count > 0 { r.Recorder.Event(&user, "Normal", "Changed", fmt.Sprintf("%d queries executed", count)) sqlOperatorQueries.With(promLabels).Add(float64(count)) + + if !user.Status.Created { + user.Status.Created = true + user.Status.CreationTimestamp = &metav1.Time{Time: time.Now()} + } + user.Status.LastModifiedTimestamp = &metav1.Time{Time: time.Now()} + user.Status.OwnerID = driver.GetOwnerID() + + if err := r.Status().Update(ctx, &user); err != nil { + log.Error(err, "unable to update SqlUser status") + return ctrl.Result{}, err + } } return scheduledResult, nil diff --git a/docs/extras/ownership.md b/docs/extras/ownership.md new file mode 100644 index 0000000..91b61a9 --- /dev/null +++ b/docs/extras/ownership.md @@ -0,0 +1,21 @@ +# Ownership + +Sql-operator comes with ownership tracking since +[v1.13.0](https://github.com/stenic/sql-operator/releases/tag/v1.13.0). Ownership tracking ensures that only +1 object can alter the Sql resources. + +Each time an object gets reconciled, a check will be performed to validate that the kubernetes object is indeed +the object managing the Sql resource. An additional status field has been added called `OwnerID`. The tracking +is implemented on each driver. + +The following checks are performed: + +``` +Is the object known in the reference table? + -> YES: Does the OwnerID match? + -> YES: IsOwner + -> NO: NotOwner + -> NO: Does the resource exist? + -> YES: NotOwner + -> NO: NonExisting +``` diff --git a/drivers/decorator.go b/drivers/decorator.go new file mode 100644 index 0000000..d628790 --- /dev/null +++ b/drivers/decorator.go @@ -0,0 +1,105 @@ +package drivers + +import ( + "context" + "fmt" + + "github.com/google/uuid" + steniciov1alpha1 "github.com/stenic/sql-operator/api/v1alpha1" +) + +type driverDecorator struct { + driver Driver + Noop bool + ownerShipData OwnerShipData +} + +func (d *driverDecorator) UpsertUser(ctx context.Context, user steniciov1alpha1.SqlUser) (int64, error) { + if !d.Noop { + return d.driver.UpsertUser(ctx, user) + } + return 0, nil +} + +func (d *driverDecorator) DeleteUser(ctx context.Context, user steniciov1alpha1.SqlUser) error { + if user.Spec.CleanupPolicy == steniciov1alpha1.CleanupPolicyDelete { + return d.driver.DeleteUser(ctx, user) + } + return nil +} + +func (d *driverDecorator) UpsertDatabase(ctx context.Context, database steniciov1alpha1.SqlDatabase) (int64, error) { + if !d.Noop { + return d.driver.UpsertDatabase(ctx, database) + } + return 0, nil +} + +func (d *driverDecorator) DeleteDatabase(ctx context.Context, database steniciov1alpha1.SqlDatabase) error { + if !d.Noop && database.Spec.CleanupPolicy == steniciov1alpha1.CleanupPolicyDelete { + return d.driver.DeleteDatabase(ctx, database) + } + return nil +} + +func (d *driverDecorator) UpsertGrants(ctx context.Context, grant steniciov1alpha1.SqlGrant, user steniciov1alpha1.SqlUser, database steniciov1alpha1.SqlDatabase) (int64, error) { + if !d.Noop { + return d.driver.UpsertGrants(ctx, grant, user, database) + } + return 0, nil +} + +func (d *driverDecorator) DeleteGrants(ctx context.Context, grant steniciov1alpha1.SqlGrant, user steniciov1alpha1.SqlUser, database steniciov1alpha1.SqlDatabase) error { + if !d.Noop && grant.Spec.CleanupPolicy == steniciov1alpha1.CleanupPolicyDelete { + return d.driver.DeleteGrants(ctx, grant, user, database) + } + return nil +} + +func (d *driverDecorator) DeleteOwnerState(ctx context.Context) error { + return d.driver.DeleteOwnerState(ctx, d.ownerShipData) +} + +func (d *driverDecorator) SetOwnershipData(ctx context.Context, data OwnerShipData) error { + d.ownerShipData = data + if d.ownerShipData.OwnerID == "" { + d.ownerShipData.OwnerID = steniciov1alpha1.OwnerID(uuid.New().String()) + } + + err := d.checkOwner(ctx) + if err != nil { + d.Noop = true + } + + return err +} + +func (d *driverDecorator) InitOwnerSchema(ctx context.Context) error { + return d.driver.InitOwnerSchema(ctx) +} + +func (d *driverDecorator) GetOwnerID() steniciov1alpha1.OwnerID { + return d.ownerShipData.OwnerID +} + +func (d *driverDecorator) checkOwner(ctx context.Context) error { + ownerState, err := d.driver.GetOwnerState(ctx, d.ownerShipData) + if err != nil { + return err + } + switch ownerState { + case NonExisting: + if err := d.driver.SetOwnerState(ctx, d.ownerShipData); err != nil { + return fmt.Errorf("failed to claim ownership - %s", err.Error()) + } + return nil + case NotOwner: + // We are not managing this resource) + return fmt.Errorf("resource already exists and will not managed by this sqlOperator") + case IsOwner: + // All good, proceed + return nil + default: + return fmt.Errorf("unknown ownerState '%s'", ownerState) + } +} diff --git a/drivers/driver.go b/drivers/driver.go index ff70dc6..d857343 100644 --- a/drivers/driver.go +++ b/drivers/driver.go @@ -9,17 +9,45 @@ import ( steniciov1alpha1 "github.com/stenic/sql-operator/api/v1alpha1" ) -func GetDriver(host steniciov1alpha1.SqlHost) (Driver, error) { +func GetDriver(host steniciov1alpha1.SqlHost) (*driverDecorator, error) { + var driver Driver switch host.Spec.Engine { case steniciov1alpha1.EngineTypeMysql: - return &MySqlDriver{ + driver = &MySqlDriver{ Host: host, + } + } + + if driver != nil { + return &driverDecorator{ + driver: driver, }, nil } return nil, errors.New("Driver could not be resolved") } +type OwnerShipType string + +type OwnerShipData struct { + Type OwnerShipType + Name string + Resource string + OwnerID steniciov1alpha1.OwnerID +} + +type OwnerState string + +const ( + IsOwner OwnerState = "IsOwner" + NotOwner OwnerState = "NotOwner" + NonExisting OwnerState = "NonExisting" + + OwnerShipTypeDatabase OwnerShipType = "OwnerShipTypeDatabase" + OwnerShipTypeUser OwnerShipType = "OwnerShipTypeUser" + OwnerShipTypeGrant OwnerShipType = "OwnerShipTypeGrant" +) + type Driver interface { UpsertUser(context.Context, steniciov1alpha1.SqlUser) (int64, error) DeleteUser(context.Context, steniciov1alpha1.SqlUser) error @@ -27,4 +55,8 @@ type Driver interface { DeleteDatabase(context.Context, steniciov1alpha1.SqlDatabase) error UpsertGrants(context.Context, steniciov1alpha1.SqlGrant, steniciov1alpha1.SqlUser, steniciov1alpha1.SqlDatabase) (int64, error) DeleteGrants(context.Context, steniciov1alpha1.SqlGrant, steniciov1alpha1.SqlUser, steniciov1alpha1.SqlDatabase) error + InitOwnerSchema(context.Context) error + SetOwnerState(context.Context, OwnerShipData) error + DeleteOwnerState(context.Context, OwnerShipData) error + GetOwnerState(context.Context, OwnerShipData) (OwnerState, error) } diff --git a/drivers/mysql.go b/drivers/mysql.go index 9f4ab4b..c5cab1e 100644 --- a/drivers/mysql.go +++ b/drivers/mysql.go @@ -3,7 +3,6 @@ package drivers import ( "context" "database/sql" - "encoding/json" "fmt" "regexp" "strings" @@ -14,6 +13,22 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) +var refSchema = "sqloperator_schema" +var refSchemaDatabase = "sqldatabase_ref" +var refSchemaUser = "sqluser_ref" +var refSchemaGrant = "sqlgrant_ref" + +var createOwnerSchema = fmt.Sprintf(`CREATE DATABASE IF NOT EXISTS %s;`, refSchema) +var createOwnerSchemaTableTpl = fmt.Sprintf(` +CREATE TABLE IF NOT EXISTS %s.%%s ( + id varchar(64) NOT NULL COMMENT 'Unique identifier', + name varchar(253) NOT NULL COMMENT 'Sql object name', + owner varchar(253) NOT NULL COMMENT 'Controller name', + resource varchar(507) NOT NULL COMMENT 'Kubernetes object reference', + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +`, refSchema) + type MySqlDriver struct { Host steniciov1alpha1.SqlHost } @@ -199,9 +214,126 @@ func (d *MySqlDriver) UpsertGrants(ctx context.Context, grants steniciov1alpha1. return changeCount, err } -func pp(a []string) string { - j, _ := json.Marshal(a) - return string(j) +func (d *MySqlDriver) InitOwnerSchema(ctx context.Context) error { + db, err := d.connect() + if err != nil { + return err + } + defer db.Close() + + if _, err = db.ExecContext(ctx, createOwnerSchema); err != nil { + return err + } + if _, err = db.ExecContext(ctx, fmt.Sprintf(createOwnerSchemaTableTpl, refSchemaDatabase)); err != nil { + return err + } + if _, err = db.ExecContext(ctx, fmt.Sprintf(createOwnerSchemaTableTpl, refSchemaUser)); err != nil { + return err + } + if _, err = db.ExecContext(ctx, fmt.Sprintf(createOwnerSchemaTableTpl, refSchemaGrant)); err != nil { + return err + } + return nil +} + +func (d *MySqlDriver) SetOwnerState(ctx context.Context, data OwnerShipData) error { + db, err := d.connect() + if err != nil { + return err + } + defer db.Close() + + _, err = db.ExecContext( + ctx, + fmt.Sprintf("INSERT IGNORE INTO %s (id, resource, name, owner) VALUES (?, ?, ?, ?) ;", d.getOwnerDBTable(data)), + data.OwnerID, data.Resource, data.Name, "sql-operator", + ) + + return err +} + +func (d *MySqlDriver) GetOwnerState(ctx context.Context, data OwnerShipData) (OwnerState, error) { + state := NonExisting + db, err := d.connect() + if err != nil { + return state, err + } + defer db.Close() + + sqlStatement := fmt.Sprintf("SELECT s.id FROM %s s WHERE resource=? LIMIT 1;", d.getOwnerDBTable(data)) + var dbID string + row := db.QueryRowContext(ctx, sqlStatement, data.Resource) + switch err := row.Scan(&dbID); err { + case nil: + if dbID == string(data.OwnerID) { + return IsOwner, nil + } + return NotOwner, nil + case sql.ErrNoRows: + break + default: + return NotOwner, err + } + + switch data.Type { + case OwnerShipTypeDatabase: + results, err := db.QueryContext(ctx, "SHOW databases;") + if err != nil { + return state, err + } + defer results.Close() + + for results.Next() { + var dbName string + if err = results.Scan(&dbName); err != nil { + return state, err + } + if dbName == data.Name { + return NotOwner, nil + } + } + case OwnerShipTypeUser: + sqlStatement := fmt.Sprintf("SELECT count(u.id) FROM %s s WHERE resource=? LIMIT 1;", d.getOwnerDBTable(data)) + var matchCount int + row := db.QueryRowContext(ctx, sqlStatement, data.Resource) + if err := row.Scan(&matchCount); err != nil { + return NotOwner, err + } + if matchCount >= 1 { + return NotOwner, nil + } + } + + return NonExisting, nil +} + +func (d *MySqlDriver) DeleteOwnerState(ctx context.Context, data OwnerShipData) error { + db, err := d.connect() + if err != nil { + return err + } + defer db.Close() + + _, err = db.ExecContext( + ctx, + fmt.Sprintf("DELETE FROM %s WHERE id = ? AND resource = ? LIMIT 1;", d.getOwnerDBTable(data)), + data.OwnerID, data.Resource, + ) + + return err +} + +func (d *MySqlDriver) getOwnerDBTable(data OwnerShipData) string { + switch data.Type { + case OwnerShipTypeDatabase: + return fmt.Sprintf("`%s`.%s", refSchema, refSchemaDatabase) + case OwnerShipTypeUser: + return fmt.Sprintf("`%s`.%s", refSchema, refSchemaUser) + case OwnerShipTypeGrant: + return fmt.Sprintf("`%s`.%s", refSchema, refSchemaGrant) + default: + panic("unhandled ownership type") + } } func difference(a, b []string) []string { diff --git a/go.mod b/go.mod index 139df8e..407e3a8 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/go-sql-driver/mysql v1.6.0 + github.com/google/uuid v1.3.0 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.17.0 github.com/prometheus/client_golang v1.11.0 diff --git a/go.sum b/go.sum index 96ae376..cefb909 100644 --- a/go.sum +++ b/go.sum @@ -203,8 +203,9 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=