Skip to content

Commit

Permalink
Support templating generated secret (#161)
Browse files Browse the repository at this point in the history
  • Loading branch information
allanger authored Nov 16, 2022
1 parent aeb3b81 commit ed440c4
Show file tree
Hide file tree
Showing 11 changed files with 446 additions and 47 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ $ make k3s_mac_image
### Deploy

```
helm upgrade my-release kloeckneri/db-operator --set image.repository=my-db-operator --set image.tag=1.0.0-dev --set image.pullPolicy=IfNotPresent
helm repo add kloeckneri https://kloeckner-i.github.io/charts
helm repo update
helm upgrade my-release kloeckneri/db-operator --set image.repository=my-db-operator --set image.tag=1.0.0-dev --set image.pullPolicy=IfNotPresent --install
```

### Run unit test locally
Expand Down
5 changes: 3 additions & 2 deletions api/v1alpha1/database_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ type DatabaseSpec struct {
// These keywords can be used: Protocol, DatabaseHost, DatabasePort, UserName, Password, DatabaseName.
// Default template looks like this:
// "{{ .Protocol }}://{{ .UserName }}:{{ .Password }}@{{ .DatabaseHost }}:{{ .DatabasePort }}/{{ .DatabaseName }}"
ConnectionStringTemplate string `json:"connectionStringTemplate,omitempty"`
Postgres Postgres `json:"postgres,omitempty"`
ConnectionStringTemplate string `json:"connectionStringTemplate,omitempty"`
SecretsTemplates map[string]string `json:"secretsTemplates,omitempty"`
Postgres Postgres `json:"postgres,omitempty"`
}

// Postgres struct should be used to provide resource that only applicable to postgres
Expand Down
29 changes: 29 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

4 changes: 4 additions & 0 deletions config/crd/bases/kci.rocks_databases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ spec:
type: object
secretName:
type: string
secretsTemplates:
additionalProperties:
type: string
type: object
required:
- backup
- deletionProtected
Expand Down
64 changes: 48 additions & 16 deletions controllers/database_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ var (
dbPhaseCreate = "Creating"
dbPhaseInstanceAccessSecret = "InstanceAccessSecretCreating"
dbPhaseProxy = "ProxyCreating"
dbPhaseConnectionString = "ConnectionStringCreating"
dbPhaseSecretsTemplating = "SecretsTemplating"
dbPhaseConfigMap = "InfoConfigMapCreating"
dbPhaseMonitoring = "MonitoringCreating"
dbPhaseBackupJob = "BackupJobCreating"
Expand Down Expand Up @@ -124,7 +124,6 @@ func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
// finalization logic fails, don't remove the finalizer so
// that we can retry during the next reconciliation.
if containsString(dbcr.ObjectMeta.Finalizers, "db."+dbcr.Name) {
logrus.Infof("DB: namespace=%s, name=%s deleting database", dbcr.Namespace, dbcr.Name)
err := r.deleteDatabase(ctx, dbcr)
if err != nil {
logrus.Errorf("DB: namespace=%s, name=%s failed deleting database - %s", dbcr.Namespace, dbcr.Name, err)
Expand Down Expand Up @@ -205,9 +204,9 @@ func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
if err != nil {
return r.manageError(ctx, dbcr, err, true)
}
dbcr.Status.Phase = dbPhaseConnectionString
case dbPhaseConnectionString:
err := r.createConnectionString(ctx, dbcr)
dbcr.Status.Phase = dbPhaseSecretsTemplating
case dbPhaseSecretsTemplating:
err := r.createTemplatedSecrets(ctx, dbcr)
if err != nil {
return r.manageError(ctx, dbcr, err, true)
}
Expand Down Expand Up @@ -352,7 +351,6 @@ func (r *DatabaseReconciler) createDatabase(ctx context.Context, dbcr *kciv1alph

err = database.Create(db, adminCred)
if err != nil {

return err
}

Expand Down Expand Up @@ -591,34 +589,68 @@ func (r *DatabaseReconciler) createProxy(ctx context.Context, dbcr *kciv1alpha1.
return nil
}

func (r *DatabaseReconciler) createConnectionString(ctx context.Context, dbcr *kciv1alpha1.Database) error {
func (r *DatabaseReconciler) createTemplatedSecrets(ctx context.Context, dbcr *kciv1alpha1.Database) error {
// First of all the password should be taken from secret because it's not stored anywhere else
databaseSecret, err := r.getDatabaseSecret(ctx, dbcr)
if err != nil {
return err
}
// Then parse the secret to get the password
databaseCred, err := parseDatabaseSecretData(dbcr, databaseSecret.Data)
// Connection stirng is deprecated and will be removed soon. So this switch is temporary.
// Once connection string is removed, the switch and the following if condition are gone
useLegacyConnectionString := false
switch {
case len(dbcr.Spec.ConnectionStringTemplate) > 0 && len(dbcr.Spec.SecretsTemplates) > 0:
logrus.Warnf("DB: namespace=%s, name=%s connectionStringTemplate will be ignored since secretsTemplates is not empty",
dbcr.Namespace,
dbcr.Name,
)
case len(dbcr.Spec.ConnectionStringTemplate) > 0:
logrus.Warnf("DB: namespace=%s, name=%s connectionStringTemplate is deprecated and will be removed in the near future, consider using secretsTemplates",
dbcr.Namespace,
dbcr.Name,
)
useLegacyConnectionString = true
default:
logrus.Infof("DB: namespace=%s, name=%s generating secrets", dbcr.Namespace, dbcr.Name)
}

databaseCred, err := parseTemplatedSecretsData(dbcr, databaseSecret.Data, useLegacyConnectionString)
if err != nil {
return err
}

// Generate the connection string
dbConnectionString, err := generateConnectionString(dbcr, databaseCred)
if useLegacyConnectionString {
// Generate the connection string
dbConnectionString, err := generateConnectionString(dbcr, databaseCred)
if err != nil {
return err
}
// Update database-credentials secret.
if databaseCred.TemplatedSecrets["CONNECTION_STRING"] == dbConnectionString {
return nil
}
logrus.Debugf("DB: namespace=%s, name=%s updating credentials secret", dbcr.Namespace, dbcr.Name)
newSecret := addConnectionStringToSecret(dbcr, databaseSecret.Data, dbConnectionString)
return r.Update(ctx, newSecret, &client.UpdateOptions{})
}

dbSecrets, err := generateTemplatedSecrets(dbcr, databaseCred)
if err != nil {
return err
}
// Update database-credentials secret.
if databaseCred.ConnectionString == dbConnectionString {
return nil
// Adding values
newSecret := fillTemplatedSecretData(dbcr, databaseSecret.Data, dbSecrets)
err = r.Update(ctx, newSecret, &client.UpdateOptions{})
if err != nil {
return err
}
logrus.Debugf("DB: namespace=%s, name=%s updating credentials secret", dbcr.Namespace, dbcr.Name)
newSecret := addConnectionStringToSecret(dbcr, databaseSecret.Data, dbConnectionString)
newSecret = removeObsoleteSecret(dbcr, databaseSecret.Data, dbSecrets)
err = r.Update(ctx, newSecret, &client.UpdateOptions{})
if err != nil {
return err
}
logrus.Infof("DB: namespace=%s, name=%s connection string is added to credentials secret", dbcr.Namespace, dbcr.Name)

return nil
}

Expand Down
151 changes: 141 additions & 10 deletions controllers/database_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ import (
"github.com/kloeckner-i/db-operator/pkg/utils/kci"
"github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
"k8s.io/utils/strings/slices"
)

// ConnectionStringFields defines default fields that can be used to generate a connection string
type ConnectionStringFields struct {
// SecretsTemplatesFields defines default fields that can be used to generate secrets with db creds
type SecretsTemplatesFields struct {
Protocol string
DatabaseHost string
DatabasePort int32
Expand All @@ -39,6 +40,19 @@ type ConnectionStringFields struct {
DatabaseName string
}

const (
fieldPostgresDB = "POSTGRES_DB"
fieldPostgresUser = "POSTGRES_USER"
fieldPostgressPassword = "POSTGRES_PASSWORD"
fieldMysqlDB = "DB"
fieldMysqlUser = "USER"
fieldMysqlPassword = "PASSWORD"
)

func getBlockedTempatedKeys() []string {
return []string{fieldMysqlDB, fieldMysqlPassword, fieldMysqlUser, fieldPostgresDB, fieldPostgresUser, fieldPostgressPassword}
}

func determinDatabaseType(dbcr *kciv1alpha1.Database, dbCred database.Credentials) (database.Database, error) {
instance, err := dbcr.GetInstanceRef()
if err != nil {
Expand Down Expand Up @@ -110,18 +124,43 @@ func determinDatabaseType(dbcr *kciv1alpha1.Database, dbCred database.Credential
}
}

func parseDatabaseSecretData(dbcr *kciv1alpha1.Database, data map[string][]byte) (database.Credentials, error) {
cred := database.Credentials{}
engine, err := dbcr.GetEngineType()
func parseTemplatedSecretsData(dbcr *kciv1alpha1.Database, data map[string][]byte, useLegacyConnStr bool) (database.Credentials, error) {
cred, err := parseDatabaseSecretData(dbcr, data)
if err != nil {
return cred, err
}
cred.TemplatedSecrets = map[string]string{}

// Connection string can be empty
if connectionString, ok := data["CONNECTION_STRING"]; ok {
cred.ConnectionString = string(connectionString)
if useLegacyConnStr {
if connectionString, ok := data["CONNECTION_STRING"]; ok {
cred.TemplatedSecrets["CONNECTION_STRING"] = string(connectionString)
} else {
logrus.Infof("DB: namespace=%s, name=%s CONNECTION_STRING key does not exist in the secret data", dbcr.Namespace, dbcr.Name)
}
} else {
logrus.Info("CONNECTION_STRING key does not exist in secret data")
for key := range dbcr.Spec.SecretsTemplates {
// Here we can see if there are obsolete entries in the secret data
if secret, ok := data[key]; ok {
delete(data, key)
cred.TemplatedSecrets[key] = string(secret)
} else {
logrus.Infof("DB: namespace=%s, name=%s %s key does not exist in secret data",
dbcr.Namespace,
dbcr.Name,
key,
)
}
}
}

return cred, nil
}

func parseDatabaseSecretData(dbcr *kciv1alpha1.Database, data map[string][]byte) (database.Credentials, error) {
cred := database.Credentials{}
engine, err := dbcr.GetEngineType()
if err != nil {
return cred, err
}

switch engine {
Expand Down Expand Up @@ -211,7 +250,7 @@ func generateConnectionString(dbcr *kciv1alpha1.Database, databaseCred database.
// "postgresql://user:password@host:port/database"
const defaultTemplate = "{{ .Protocol }}://{{ .UserName }}:{{ .Password }}@{{ .DatabaseHost }}:{{ .DatabasePort }}/{{ .DatabaseName }}"

dbData := ConnectionStringFields{
dbData := SecretsTemplatesFields{
DatabaseHost: dbcr.Status.ProxyStatus.ServiceName,
DatabasePort: dbcr.Status.ProxyStatus.SQLPort,
UserName: databaseCred.Username,
Expand Down Expand Up @@ -262,7 +301,99 @@ func generateConnectionString(dbcr *kciv1alpha1.Database, databaseCred database.
return
}

func generateTemplatedSecrets(dbcr *kciv1alpha1.Database, databaseCred database.Credentials) (secrets map[string]string, err error) {
secrets = map[string]string{}
templates := map[string]string{}
if len(dbcr.Spec.SecretsTemplates) > 0 {
templates = dbcr.Spec.SecretsTemplates
} else {
const tmpl = "{{ .Protocol }}://{{ .UserName }}:{{ .Password }}@{{ .DatabaseHost }}:{{ .DatabasePort }}/{{ .DatabaseName }}"
templates["CONNECTION_STRING"] = tmpl
}
// The string that's going to be generated if the default template is used:
// "postgresql://user:password@host:port/database"
dbData := SecretsTemplatesFields{
DatabaseHost: dbcr.Status.ProxyStatus.ServiceName,
DatabasePort: dbcr.Status.ProxyStatus.SQLPort,
UserName: databaseCred.Username,
Password: databaseCred.Password,
DatabaseName: databaseCred.Name,
}

// If proxy is not used, set a real database address
if !dbcr.Status.ProxyStatus.Status {
db, err := determinDatabaseType(dbcr, databaseCred)
if err != nil {
return nil, err
}
dbAddress := db.GetDatabaseAddress()
dbData.DatabaseHost = dbAddress.Host
dbData.DatabasePort = int32(dbAddress.Port)
}
// If engine is 'postgres', the protocol should be postgresql
if dbcr.Status.InstanceRef.Spec.Engine == "postgres" {
dbData.Protocol = "postgresql"
} else {
dbData.Protocol = dbcr.Status.InstanceRef.Spec.Engine
}

logrus.Infof("DB: namespace=%s, name=%s creating secrets from templates", dbcr.Namespace, dbcr.Name)
for key, value := range templates {
var tmpl string = value
t, err := template.New("secret").Parse(tmpl)
if err != nil {
return nil, err
}

var secretBytes bytes.Buffer
err = t.Execute(&secretBytes, dbData)
if err != nil {
return nil, err
}
connString := secretBytes.String()
secrets[key] = connString
}
return secrets, nil
}

func fillTemplatedSecretData(dbcr *kciv1alpha1.Database, secretData map[string][]byte, newSecretFields map[string]string) (newSecret *v1.Secret) {
blockedTempatedKeys := getBlockedTempatedKeys()
for key, value := range newSecretFields {
if slices.Contains(blockedTempatedKeys, key) {
logrus.Warnf("DB: namespace=%s, name=%s %s can't be used for templating, because it's used for default secret created by operator",
dbcr.Namespace,
dbcr.Name,
key,
)
} else {
newSecret = addTemplatedSecretToSecret(dbcr, secretData, key, value)
}
}
return
}

func addConnectionStringToSecret(dbcr *kciv1alpha1.Database, secretData map[string][]byte, connectionString string) *v1.Secret {
secretData["CONNECTION_STRING"] = []byte(connectionString)
return kci.SecretBuilder(dbcr.Spec.SecretName, dbcr.GetNamespace(), secretData)
}

func addTemplatedSecretToSecret(dbcr *kciv1alpha1.Database, secretData map[string][]byte, secretName string, secretValue string) *v1.Secret {
secretData[secretName] = []byte(secretValue)
return kci.SecretBuilder(dbcr.Spec.SecretName, dbcr.GetNamespace(), secretData)
}

func removeObsoleteSecret(dbcr *kciv1alpha1.Database, secretData map[string][]byte, newSecretFields map[string]string) *v1.Secret {
blockedTempatedKeys := getBlockedTempatedKeys()

for key := range secretData {
if _, ok := newSecretFields[key]; !ok {
// Check if is a untemplatead secret, so it's not removed accidentally
if !slices.Contains(blockedTempatedKeys, key) {
logrus.Infof("DB: namespace=%s, name=%s removing an obsolete field: %s", dbcr.Namespace, dbcr.Name, key)
delete(secretData, key)
}
}
}

return kci.SecretBuilder(dbcr.Spec.SecretName, dbcr.GetNamespace(), secretData)
}
Loading

0 comments on commit ed440c4

Please sign in to comment.