Skip to content

Support templating generated secret #161

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

Merged
merged 18 commits into from
Nov 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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