-
Notifications
You must be signed in to change notification settings - Fork 174
ROX-12934: Fix operator reconciliation when external DB is in use #3322
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c393b13
f4bcbfc
1f6771d
c76f548
c698c56
98ecab2
5a244a5
202e00b
67d0e1d
d77bcc1
f0a52dd
46feaf3
6af61ed
bdbbdbd
e33b999
e3059f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ package extensions | |||||
|
|
||||||
| import ( | ||||||
| "context" | ||||||
| "strings" | ||||||
|
|
||||||
| "github.com/go-logr/logr" | ||||||
| "github.com/operator-framework/helm-operator-plugins/pkg/extensions" | ||||||
|
|
@@ -10,57 +11,116 @@ import ( | |||||
| commonExtensions "github.com/stackrox/rox/operator/pkg/common/extensions" | ||||||
| "github.com/stackrox/rox/operator/pkg/types" | ||||||
| "github.com/stackrox/rox/pkg/renderer" | ||||||
| coreV1 "k8s.io/api/core/v1" | ||||||
| ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" | ||||||
| ) | ||||||
|
|
||||||
| const ( | ||||||
| centralDBPasswordKey = `password` | ||||||
| centralDBPasswordResourceName = "central-db-password" | ||||||
| centralDBPasswordKey = `password` | ||||||
|
|
||||||
| // canonicalCentralDBPasswordSecretName is the name of the secret that is mounted into Central (and Central DB). | ||||||
| // This is not configurable; if a user specifies a different password secret, the password from that needs to be | ||||||
| // mirrored into the canonical password secret. | ||||||
| canonicalCentralDBPasswordSecretName = `central-db-password` | ||||||
| ) | ||||||
|
|
||||||
| // ReconcileCentralDBPasswordExtension returns an extension that takes care of creating the central-db-password | ||||||
| // secret ahead of time. | ||||||
| // ReconcileCentralDBPasswordExtension returns an extension that takes care of reconciling the central-db-password secret. | ||||||
| func ReconcileCentralDBPasswordExtension(client ctrlClient.Client) extensions.ReconcileExtension { | ||||||
| return wrapExtension(reconcileCentralDBPassword, client) | ||||||
| } | ||||||
|
|
||||||
| func reconcileCentralDBPassword(ctx context.Context, c *platform.Central, client ctrlClient.Client, _ func(updateStatusFunc), _ logr.Logger) error { | ||||||
| if !c.Spec.Central.CentralDBEnabled() || c.Spec.Central.DB.IsExternal() { | ||||||
| return nil | ||||||
| } | ||||||
| func reconcileCentralDBPassword(ctx context.Context, c *platform.Central, client ctrlClient.Client, statusUpdater func(updateStatusFunc), log logr.Logger) error { | ||||||
| run := &reconcileCentralDBPasswordExtensionRun{ | ||||||
| SecretReconciliator: commonExtensions.NewSecretReconciliator(client, c), | ||||||
| obj: c, | ||||||
| centralObj: c, | ||||||
| } | ||||||
| return run.Execute(ctx) | ||||||
| } | ||||||
|
|
||||||
| type reconcileCentralDBPasswordExtensionRun struct { | ||||||
| *commonExtensions.SecretReconciliator | ||||||
| obj *platform.Central | ||||||
| centralObj *platform.Central | ||||||
| password string | ||||||
| } | ||||||
|
|
||||||
| func (r *reconcileCentralDBPasswordExtensionRun) readAndSetPasswordFromReferencedSecret(ctx context.Context) error { | ||||||
| if r.centralObj.Spec.Central.DB.GetPasswordSecret() == nil { | ||||||
| return errors.New("no password secret was specified in spec.central.db.passwordSecret") | ||||||
| } | ||||||
|
|
||||||
| passwordSecretName := r.centralObj.Spec.Central.DB.PasswordSecret.Name | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this only makes sense if there's a nil-safe getter for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. actually, we already check that |
||||||
|
|
||||||
| passwordSecret := &coreV1.Secret{} | ||||||
| key := ctrlClient.ObjectKey{Namespace: r.centralObj.GetNamespace(), Name: passwordSecretName} | ||||||
| if err := r.Client().Get(ctx, key, passwordSecret); err != nil { | ||||||
| return errors.Wrapf(err, "failed to retrieve central db password secret %q", passwordSecretName) | ||||||
| } | ||||||
|
|
||||||
| password, err := passwordFromSecretData(passwordSecret.Data) | ||||||
| if err != nil { | ||||||
| return errors.Wrapf(err, "reading central db password from secret %s", passwordSecretName) | ||||||
| } | ||||||
|
|
||||||
| r.password = password | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not returning the password and setting it from the main function? It feels odd to me because the result of that function is not returned, instead in sets state on the struct.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can change it. I don't feel very strongly tbh because the reconciliation execution structs are heavily dependent on modifying state. For example, validate also sets the current password, and this is intended.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I think it makes more sense to leave the behavior as-is. I will rename the function to |
||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| func (r *reconcileCentralDBPasswordExtensionRun) Execute(ctx context.Context) error { | ||||||
| // Delete any central-db password only if the CR is being deleted | ||||||
| shouldExist := r.obj.GetDeletionTimestamp() == nil | ||||||
| if r.centralObj.DeletionTimestamp != nil || !r.centralObj.Spec.Central.CentralDBEnabled() { | ||||||
| return r.ReconcileSecret(ctx, canonicalCentralDBPasswordSecretName, false, nil, nil, false) | ||||||
| } | ||||||
|
|
||||||
| if err := r.ReconcileSecret(ctx, centralDBPasswordResourceName, shouldExist, r.validateCentralDBPasswordData, r.generateCentralDBPasswordData, true); err != nil { | ||||||
| return errors.Wrapf(err, "reconciling %q secret", centralDBPasswordResourceName) | ||||||
| dbSpec := r.centralObj.Spec.Central.DB // non-nil thanks to CentralDBEnabled() check above | ||||||
| dbPasswordSecret := dbSpec.PasswordSecret | ||||||
porridge marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| if dbSpec.IsExternal() && dbPasswordSecret == nil { | ||||||
| return errors.New("setting spec.central.db.passwordSecret is mandatory when using an external DB") | ||||||
| } | ||||||
|
|
||||||
| if dbPasswordSecret != nil { | ||||||
| if err := r.readAndSetPasswordFromReferencedSecret(ctx); err != nil { | ||||||
| return err | ||||||
| } | ||||||
| // If the user wants to use the central-db-password secret directly, that's fine, and we don't have anything more to do. | ||||||
| if dbPasswordSecret.Name == canonicalCentralDBPasswordSecretName { | ||||||
| return nil | ||||||
| } | ||||||
| } | ||||||
| // At this point, r.password was set via readAndSetPasswordFromReferencedSecret above (user-specified mode), or is unset, | ||||||
| // in which case the auto-generation logic will take effect. | ||||||
| if err := r.ReconcileSecret(ctx, canonicalCentralDBPasswordSecretName, true, r.validateSecretData, r.generateDBPassword, true); err != nil { | ||||||
| return errors.Wrapf(err, "reconciling %s secret", canonicalCentralDBPasswordSecretName) | ||||||
| } | ||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| func (r *reconcileCentralDBPasswordExtensionRun) validateCentralDBPasswordData(data types.SecretDataMap, _ bool) error { | ||||||
| if len(data[centralDBPasswordKey]) == 0 { | ||||||
| return errors.Errorf("%s secret must contain a non-empty %q entry", centralDBPasswordResourceName, centralDBPasswordKey) | ||||||
| func (r *reconcileCentralDBPasswordExtensionRun) validateSecretData(data types.SecretDataMap, controllerOwned bool) error { | ||||||
| password, err := passwordFromSecretData(data) | ||||||
| if err != nil { | ||||||
| return errors.Wrap(err, "validating existing secret data") | ||||||
| } | ||||||
| if r.password != "" && r.password != password { | ||||||
| return errors.New("existing password does not match expected one") | ||||||
| } | ||||||
| // The following assignment shouldn't have any consequences, as a successful validation should prevent generation | ||||||
| // from being invoked, but better safe than sorry (about clobbering a user-set password). | ||||||
| r.password = password | ||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| func (r *reconcileCentralDBPasswordExtensionRun) generateCentralDBPasswordData() (types.SecretDataMap, error) { | ||||||
| data := types.SecretDataMap{ | ||||||
| centralDBPasswordKey: []byte(renderer.CreatePassword()), | ||||||
| func (r *reconcileCentralDBPasswordExtensionRun) generateDBPassword() (types.SecretDataMap, error) { | ||||||
| if r.password == "" { | ||||||
| r.password = renderer.CreatePassword() | ||||||
| } | ||||||
|
|
||||||
| return types.SecretDataMap{ | ||||||
| centralDBPasswordKey: []byte(r.password), | ||||||
| }, nil | ||||||
| } | ||||||
|
|
||||||
| func passwordFromSecretData(data types.SecretDataMap) (string, error) { | ||||||
| password := strings.TrimSpace(string(data[centralDBPasswordKey])) | ||||||
| if password == "" || strings.ContainsAny(password, "\r\n") { | ||||||
| return "", errors.Errorf("secret must contain a non-empty, single-line %q entry", centralDBPasswordKey) | ||||||
| } | ||||||
| return data, nil | ||||||
| return password, nil | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.