Skip to content

Commit

Permalink
feat: Resettable password for RDS
Browse files Browse the repository at this point in the history
Signed-off-by: Paul Schroeder (external expert on behalf of DB Netz) <[email protected]>
Signed-off-by: Maximilian Blatt (external expert on behalf of DB Netz) <[email protected]>

Co-authored-by: Maximilian Blatt (external expert on behalf of DB Netz) <[email protected]>
  • Loading branch information
schroeder-paul and MisterMX committed Jul 24, 2023
1 parent 4b2ce83 commit ab4a5c4
Show file tree
Hide file tree
Showing 6 changed files with 1,610 additions and 186 deletions.
23 changes: 23 additions & 0 deletions apis/rds/v1alpha1/referencers.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package v1alpha1
import (
"context"

xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"

database "github.com/crossplane-contrib/provider-aws/apis/database/v1beta1"
network "github.com/crossplane-contrib/provider-aws/apis/ec2/v1beta1"
iamv1beta1 "github.com/crossplane-contrib/provider-aws/apis/iam/v1beta1"
Expand Down Expand Up @@ -244,3 +246,24 @@ func (mg *DBInstance) ResolveReferences(ctx context.Context, c client.Reader) er

return nil
}

// RDSClusterOrInstance interface to access common fields independent of the type
// See: https://github.com/kubernetes-sigs/controller-tools/issues/471
// +kubebuilder:object:generate=false
type RDSClusterOrInstance interface {
resource.Managed
GetMasterUserPasswordSecretRef() *xpv1.SecretKeySelector
}

// GetMasterUserPasswordSecretRef returns the MasterUserPasswordSecretRef
func (mg *DBInstance) GetMasterUserPasswordSecretRef() *xpv1.SecretKeySelector {
return mg.Spec.ForProvider.MasterUserPasswordSecretRef
}

// GetMasterUserPasswordSecretRef returns the MasterUserPasswordSecretRef
func (mg *DBCluster) GetMasterUserPasswordSecretRef() *xpv1.SecretKeySelector {
return mg.Spec.ForProvider.MasterUserPasswordSecretRef
}

var _ RDSClusterOrInstance = (*DBInstance)(nil)
var _ RDSClusterOrInstance = (*DBCluster)(nil)
200 changes: 200 additions & 0 deletions pkg/clients/rds/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
Copyright 2019 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package dbinstance

import (
"context"
"strings"

"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

svcapitypes "github.com/crossplane-contrib/provider-aws/apis/rds/v1alpha1"

xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
)

// Publicly usable variables
const (
PasswordCacheKey = "dbMasterUserPassword"
RestoreFlagCacheKay = "dbRestoreState"

RestoreStateRestored restoreSate = "RestoreStateRestored"
RestoreStateNormal restoreSate = "RestoreStateNormal"

ErrNoRetrievePasswordOrGenerate = "cannot retrieve password form masterUserPasswordSecretRef or generate a password"
ErrNoMasterUserPasswordSecretRefNorAutogenerateNoRestore = "neither a masterUserPasswordSecretRef is given, nor password autogeneration was enabled, not a restore is performed"
ErrCachePassword = "cannot cache password"
ErrNoPasswordUpToDate = "cannot determine password up to date status"
ErrGetCachedPassword = "cannot get cached password"
ErrRetrievePasswordForUpdate = "cannot retrieve password for update"
)

const (
errGetSecret = "cannot get secret"
errDeleteSecretF = "cannot delete secret %q in namespace %q"
errGetCachedPassword = "cannot get cached password"
errGetCachedRestoreInfo = "cannot get cached restore info"
errGetMasterPassword = "cannot get master password"

secretNamespace = "crossplane-system"
)

type restoreSate string

// Cache caches the given key/value map to the corresponding cache secret
func Cache(ctx context.Context, kube client.Client, mg resource.Managed, kv map[string]string) (sc *corev1.Secret, err error) {
return updateOrCreateSecret(ctx, kube, getCachingSecretRef(mg), kv)
}

// DeleteCache removes the (secret) cache
func DeleteCache(ctx context.Context, kube client.Client, mg resource.Managed) (err error) {
ref := getCachingSecretRef(mg)

secret := new(corev1.Secret)
secret.Name = ref.Name
secret.Namespace = ref.Namespace

err = kube.Delete(ctx, secret)
if resource.IgnoreNotFound(err) != nil {
return errors.Wrapf(err, errDeleteSecretF, ref.Name, ref.Namespace)
}
return nil
}

// GetSecretValue fetches the referenced input secret key reference
func GetSecretValue(ctx context.Context, kube client.Client, ref *xpv1.SecretKeySelector) (val string, err error) {
secret, err := getSecret(ctx, kube, ref.SecretReference)
if resource.IgnoreNotFound(err) != nil {
return "", errors.Wrap(err, errGetSecret)
}

pwRaw := secret.Data[ref.Key]
return string(pwRaw), nil
}

// GetDesiredPassword calculates the desired password from cache/masterPasswordSecretRef
func GetDesiredPassword(ctx context.Context, kube client.Client, cr svcapitypes.RDSClusterOrInstance) (desiredPassword string, err error) {
cachedPassword, err := getCachedPassword(ctx, kube, cr)
if err != nil {
return "", errors.Wrap(err, errGetCachedPassword)
}
if cr.GetMasterUserPasswordSecretRef() != nil {
desiredPassword, err = GetSecretValue(ctx, kube, cr.GetMasterUserPasswordSecretRef())
if err != nil {
return "", errors.Wrap(err, errGetMasterPassword)
}
}
if desiredPassword == "" {
desiredPassword = cachedPassword
}
return desiredPassword, nil
}

// PasswordUpToDate tell whether the password is up-to-date (depends on restore, masterPasswordSecretRef and cached password)
func PasswordUpToDate(ctx context.Context, kube client.Client, cr svcapitypes.RDSClusterOrInstance) (upToDate bool, err error) {
// (schroeder-paul): We are checking password changes after the database is ready.
// - the restore scenario: if the new database has a different password than the old one, the old password will be
// changed to the new one which was set or autogenerated (and cached) from the preCreate step.
// - the user wants to change the password by changing the MasterUserPasswordSecretRef secret
var desiredPassword string

restoreInfo, err := getCachedRestoreInfo(ctx, kube, cr)
if err != nil {
return false, errors.Wrap(err, errGetCachedRestoreInfo)
}
cachedPassword, err := getCachedPassword(ctx, kube, cr)
if err != nil {
return false, errors.Wrap(err, errGetCachedPassword)
}
if cr.GetMasterUserPasswordSecretRef() != nil {
desiredPassword, err = GetSecretValue(ctx, kube, cr.GetMasterUserPasswordSecretRef())
if err != nil {
return false, errors.Wrap(err, errGetMasterPassword)
}
}

wasRestored := restoreInfo == RestoreStateRestored
passwordFromSecret := desiredPassword != ""
secretPasswordMatchesCachedPassword := desiredPassword == cachedPassword
newPasswordFromSecret := passwordFromSecret && !secretPasswordMatchesCachedPassword
upToDate = !(newPasswordFromSecret || wasRestored)

return upToDate, err
}

func getCachedRestoreInfo(ctx context.Context, kube client.Client, mg resource.Managed) (state restoreSate, err error) {
secretKeyRef := &xpv1.SecretKeySelector{
SecretReference: getCachingSecretRef(mg),
Key: RestoreFlagCacheKay,
}
restoreInfo, err := GetSecretValue(ctx, kube, secretKeyRef)

switch restoreInfo {
case string(RestoreStateRestored):
state = RestoreStateRestored
default:
state = RestoreStateNormal
}

return state, err
}

func getCachedPassword(ctx context.Context, kube client.Client, mg resource.Managed) (pw string, err error) {
secretKeyRef := &xpv1.SecretKeySelector{
SecretReference: getCachingSecretRef(mg),
Key: PasswordCacheKey,
}
return GetSecretValue(ctx, kube, secretKeyRef)
}

func getCachingSecretRef(mg resource.Managed) xpv1.SecretReference {
secretName := mg.GetObjectKind().GroupVersionKind().Kind + "." + string(mg.GetUID())
secretName = strings.ToLower(secretName)

return xpv1.SecretReference{
Name: secretName,
Namespace: secretNamespace,
}
}

func updateOrCreateSecret(ctx context.Context, kube client.Client, ref xpv1.SecretReference, kv map[string]string) (*corev1.Secret, error) {
data := make(map[string][]byte, len(kv))
for k, v := range kv {
data[k] = []byte(v)
}

sc := &corev1.Secret{
Data: data,
ObjectMeta: metav1.ObjectMeta{
Name: ref.Name,
Namespace: ref.Namespace,
},
}
err := resource.NewAPIPatchingApplicator(kube).Apply(ctx, sc)
return sc, err
}

func getSecret(ctx context.Context, kube client.Client, ref xpv1.SecretReference) (*corev1.Secret, error) {
secret := new(corev1.Secret)
err := kube.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: ref.Namespace}, secret)
return secret, err
}
Loading

0 comments on commit ab4a5c4

Please sign in to comment.