Skip to content

Commit ed440c4

Browse files
authored
Support templating generated secret (#161)
1 parent aeb3b81 commit ed440c4

File tree

11 files changed

+446
-47
lines changed

11 files changed

+446
-47
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ $ make k3s_mac_image
117117
### Deploy
118118

119119
```
120-
helm upgrade my-release kloeckneri/db-operator --set image.repository=my-db-operator --set image.tag=1.0.0-dev --set image.pullPolicy=IfNotPresent
120+
helm repo add kloeckneri https://kloeckner-i.github.io/charts
121+
helm repo update
122+
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
121123
```
122124

123125
### Run unit test locally

api/v1alpha1/database_types.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ type DatabaseSpec struct {
3535
// These keywords can be used: Protocol, DatabaseHost, DatabasePort, UserName, Password, DatabaseName.
3636
// Default template looks like this:
3737
// "{{ .Protocol }}://{{ .UserName }}:{{ .Password }}@{{ .DatabaseHost }}:{{ .DatabasePort }}/{{ .DatabaseName }}"
38-
ConnectionStringTemplate string `json:"connectionStringTemplate,omitempty"`
39-
Postgres Postgres `json:"postgres,omitempty"`
38+
ConnectionStringTemplate string `json:"connectionStringTemplate,omitempty"`
39+
SecretsTemplates map[string]string `json:"secretsTemplates,omitempty"`
40+
Postgres Postgres `json:"postgres,omitempty"`
4041
}
4142

4243
// Postgres struct should be used to provide resource that only applicable to postgres

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/kci.rocks_databases.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ spec:
104104
type: object
105105
secretName:
106106
type: string
107+
secretsTemplates:
108+
additionalProperties:
109+
type: string
110+
type: object
107111
required:
108112
- backup
109113
- deletionProtected

controllers/database_controller.go

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ var (
6363
dbPhaseCreate = "Creating"
6464
dbPhaseInstanceAccessSecret = "InstanceAccessSecretCreating"
6565
dbPhaseProxy = "ProxyCreating"
66-
dbPhaseConnectionString = "ConnectionStringCreating"
66+
dbPhaseSecretsTemplating = "SecretsTemplating"
6767
dbPhaseConfigMap = "InfoConfigMapCreating"
6868
dbPhaseMonitoring = "MonitoringCreating"
6969
dbPhaseBackupJob = "BackupJobCreating"
@@ -124,7 +124,6 @@ func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
124124
// finalization logic fails, don't remove the finalizer so
125125
// that we can retry during the next reconciliation.
126126
if containsString(dbcr.ObjectMeta.Finalizers, "db."+dbcr.Name) {
127-
logrus.Infof("DB: namespace=%s, name=%s deleting database", dbcr.Namespace, dbcr.Name)
128127
err := r.deleteDatabase(ctx, dbcr)
129128
if err != nil {
130129
logrus.Errorf("DB: namespace=%s, name=%s failed deleting database - %s", dbcr.Namespace, dbcr.Name, err)
@@ -205,9 +204,9 @@ func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
205204
if err != nil {
206205
return r.manageError(ctx, dbcr, err, true)
207206
}
208-
dbcr.Status.Phase = dbPhaseConnectionString
209-
case dbPhaseConnectionString:
210-
err := r.createConnectionString(ctx, dbcr)
207+
dbcr.Status.Phase = dbPhaseSecretsTemplating
208+
case dbPhaseSecretsTemplating:
209+
err := r.createTemplatedSecrets(ctx, dbcr)
211210
if err != nil {
212211
return r.manageError(ctx, dbcr, err, true)
213212
}
@@ -352,7 +351,6 @@ func (r *DatabaseReconciler) createDatabase(ctx context.Context, dbcr *kciv1alph
352351

353352
err = database.Create(db, adminCred)
354353
if err != nil {
355-
356354
return err
357355
}
358356

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

594-
func (r *DatabaseReconciler) createConnectionString(ctx context.Context, dbcr *kciv1alpha1.Database) error {
592+
func (r *DatabaseReconciler) createTemplatedSecrets(ctx context.Context, dbcr *kciv1alpha1.Database) error {
595593
// First of all the password should be taken from secret because it's not stored anywhere else
596594
databaseSecret, err := r.getDatabaseSecret(ctx, dbcr)
597595
if err != nil {
598596
return err
599597
}
600598
// Then parse the secret to get the password
601-
databaseCred, err := parseDatabaseSecretData(dbcr, databaseSecret.Data)
599+
// Connection stirng is deprecated and will be removed soon. So this switch is temporary.
600+
// Once connection string is removed, the switch and the following if condition are gone
601+
useLegacyConnectionString := false
602+
switch {
603+
case len(dbcr.Spec.ConnectionStringTemplate) > 0 && len(dbcr.Spec.SecretsTemplates) > 0:
604+
logrus.Warnf("DB: namespace=%s, name=%s connectionStringTemplate will be ignored since secretsTemplates is not empty",
605+
dbcr.Namespace,
606+
dbcr.Name,
607+
)
608+
case len(dbcr.Spec.ConnectionStringTemplate) > 0:
609+
logrus.Warnf("DB: namespace=%s, name=%s connectionStringTemplate is deprecated and will be removed in the near future, consider using secretsTemplates",
610+
dbcr.Namespace,
611+
dbcr.Name,
612+
)
613+
useLegacyConnectionString = true
614+
default:
615+
logrus.Infof("DB: namespace=%s, name=%s generating secrets", dbcr.Namespace, dbcr.Name)
616+
}
617+
618+
databaseCred, err := parseTemplatedSecretsData(dbcr, databaseSecret.Data, useLegacyConnectionString)
602619
if err != nil {
603620
return err
604621
}
605622

606-
// Generate the connection string
607-
dbConnectionString, err := generateConnectionString(dbcr, databaseCred)
623+
if useLegacyConnectionString {
624+
// Generate the connection string
625+
dbConnectionString, err := generateConnectionString(dbcr, databaseCred)
626+
if err != nil {
627+
return err
628+
}
629+
// Update database-credentials secret.
630+
if databaseCred.TemplatedSecrets["CONNECTION_STRING"] == dbConnectionString {
631+
return nil
632+
}
633+
logrus.Debugf("DB: namespace=%s, name=%s updating credentials secret", dbcr.Namespace, dbcr.Name)
634+
newSecret := addConnectionStringToSecret(dbcr, databaseSecret.Data, dbConnectionString)
635+
return r.Update(ctx, newSecret, &client.UpdateOptions{})
636+
}
637+
638+
dbSecrets, err := generateTemplatedSecrets(dbcr, databaseCred)
608639
if err != nil {
609640
return err
610641
}
611-
// Update database-credentials secret.
612-
if databaseCred.ConnectionString == dbConnectionString {
613-
return nil
642+
// Adding values
643+
newSecret := fillTemplatedSecretData(dbcr, databaseSecret.Data, dbSecrets)
644+
err = r.Update(ctx, newSecret, &client.UpdateOptions{})
645+
if err != nil {
646+
return err
614647
}
615-
logrus.Debugf("DB: namespace=%s, name=%s updating credentials secret", dbcr.Namespace, dbcr.Name)
616-
newSecret := addConnectionStringToSecret(dbcr, databaseSecret.Data, dbConnectionString)
648+
newSecret = removeObsoleteSecret(dbcr, databaseSecret.Data, dbSecrets)
617649
err = r.Update(ctx, newSecret, &client.UpdateOptions{})
618650
if err != nil {
619651
return err
620652
}
621-
logrus.Infof("DB: namespace=%s, name=%s connection string is added to credentials secret", dbcr.Namespace, dbcr.Name)
653+
622654
return nil
623655
}
624656

controllers/database_helper.go

Lines changed: 141 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ import (
2727
"github.com/kloeckner-i/db-operator/pkg/utils/kci"
2828
"github.com/sirupsen/logrus"
2929
v1 "k8s.io/api/core/v1"
30+
"k8s.io/utils/strings/slices"
3031
)
3132

32-
// ConnectionStringFields defines default fields that can be used to generate a connection string
33-
type ConnectionStringFields struct {
33+
// SecretsTemplatesFields defines default fields that can be used to generate secrets with db creds
34+
type SecretsTemplatesFields struct {
3435
Protocol string
3536
DatabaseHost string
3637
DatabasePort int32
@@ -39,6 +40,19 @@ type ConnectionStringFields struct {
3940
DatabaseName string
4041
}
4142

43+
const (
44+
fieldPostgresDB = "POSTGRES_DB"
45+
fieldPostgresUser = "POSTGRES_USER"
46+
fieldPostgressPassword = "POSTGRES_PASSWORD"
47+
fieldMysqlDB = "DB"
48+
fieldMysqlUser = "USER"
49+
fieldMysqlPassword = "PASSWORD"
50+
)
51+
52+
func getBlockedTempatedKeys() []string {
53+
return []string{fieldMysqlDB, fieldMysqlPassword, fieldMysqlUser, fieldPostgresDB, fieldPostgresUser, fieldPostgressPassword}
54+
}
55+
4256
func determinDatabaseType(dbcr *kciv1alpha1.Database, dbCred database.Credentials) (database.Database, error) {
4357
instance, err := dbcr.GetInstanceRef()
4458
if err != nil {
@@ -110,18 +124,43 @@ func determinDatabaseType(dbcr *kciv1alpha1.Database, dbCred database.Credential
110124
}
111125
}
112126

113-
func parseDatabaseSecretData(dbcr *kciv1alpha1.Database, data map[string][]byte) (database.Credentials, error) {
114-
cred := database.Credentials{}
115-
engine, err := dbcr.GetEngineType()
127+
func parseTemplatedSecretsData(dbcr *kciv1alpha1.Database, data map[string][]byte, useLegacyConnStr bool) (database.Credentials, error) {
128+
cred, err := parseDatabaseSecretData(dbcr, data)
116129
if err != nil {
117130
return cred, err
118131
}
132+
cred.TemplatedSecrets = map[string]string{}
119133

120-
// Connection string can be empty
121-
if connectionString, ok := data["CONNECTION_STRING"]; ok {
122-
cred.ConnectionString = string(connectionString)
134+
if useLegacyConnStr {
135+
if connectionString, ok := data["CONNECTION_STRING"]; ok {
136+
cred.TemplatedSecrets["CONNECTION_STRING"] = string(connectionString)
137+
} else {
138+
logrus.Infof("DB: namespace=%s, name=%s CONNECTION_STRING key does not exist in the secret data", dbcr.Namespace, dbcr.Name)
139+
}
123140
} else {
124-
logrus.Info("CONNECTION_STRING key does not exist in secret data")
141+
for key := range dbcr.Spec.SecretsTemplates {
142+
// Here we can see if there are obsolete entries in the secret data
143+
if secret, ok := data[key]; ok {
144+
delete(data, key)
145+
cred.TemplatedSecrets[key] = string(secret)
146+
} else {
147+
logrus.Infof("DB: namespace=%s, name=%s %s key does not exist in secret data",
148+
dbcr.Namespace,
149+
dbcr.Name,
150+
key,
151+
)
152+
}
153+
}
154+
}
155+
156+
return cred, nil
157+
}
158+
159+
func parseDatabaseSecretData(dbcr *kciv1alpha1.Database, data map[string][]byte) (database.Credentials, error) {
160+
cred := database.Credentials{}
161+
engine, err := dbcr.GetEngineType()
162+
if err != nil {
163+
return cred, err
125164
}
126165

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

214-
dbData := ConnectionStringFields{
253+
dbData := SecretsTemplatesFields{
215254
DatabaseHost: dbcr.Status.ProxyStatus.ServiceName,
216255
DatabasePort: dbcr.Status.ProxyStatus.SQLPort,
217256
UserName: databaseCred.Username,
@@ -262,7 +301,99 @@ func generateConnectionString(dbcr *kciv1alpha1.Database, databaseCred database.
262301
return
263302
}
264303

304+
func generateTemplatedSecrets(dbcr *kciv1alpha1.Database, databaseCred database.Credentials) (secrets map[string]string, err error) {
305+
secrets = map[string]string{}
306+
templates := map[string]string{}
307+
if len(dbcr.Spec.SecretsTemplates) > 0 {
308+
templates = dbcr.Spec.SecretsTemplates
309+
} else {
310+
const tmpl = "{{ .Protocol }}://{{ .UserName }}:{{ .Password }}@{{ .DatabaseHost }}:{{ .DatabasePort }}/{{ .DatabaseName }}"
311+
templates["CONNECTION_STRING"] = tmpl
312+
}
313+
// The string that's going to be generated if the default template is used:
314+
// "postgresql://user:password@host:port/database"
315+
dbData := SecretsTemplatesFields{
316+
DatabaseHost: dbcr.Status.ProxyStatus.ServiceName,
317+
DatabasePort: dbcr.Status.ProxyStatus.SQLPort,
318+
UserName: databaseCred.Username,
319+
Password: databaseCred.Password,
320+
DatabaseName: databaseCred.Name,
321+
}
322+
323+
// If proxy is not used, set a real database address
324+
if !dbcr.Status.ProxyStatus.Status {
325+
db, err := determinDatabaseType(dbcr, databaseCred)
326+
if err != nil {
327+
return nil, err
328+
}
329+
dbAddress := db.GetDatabaseAddress()
330+
dbData.DatabaseHost = dbAddress.Host
331+
dbData.DatabasePort = int32(dbAddress.Port)
332+
}
333+
// If engine is 'postgres', the protocol should be postgresql
334+
if dbcr.Status.InstanceRef.Spec.Engine == "postgres" {
335+
dbData.Protocol = "postgresql"
336+
} else {
337+
dbData.Protocol = dbcr.Status.InstanceRef.Spec.Engine
338+
}
339+
340+
logrus.Infof("DB: namespace=%s, name=%s creating secrets from templates", dbcr.Namespace, dbcr.Name)
341+
for key, value := range templates {
342+
var tmpl string = value
343+
t, err := template.New("secret").Parse(tmpl)
344+
if err != nil {
345+
return nil, err
346+
}
347+
348+
var secretBytes bytes.Buffer
349+
err = t.Execute(&secretBytes, dbData)
350+
if err != nil {
351+
return nil, err
352+
}
353+
connString := secretBytes.String()
354+
secrets[key] = connString
355+
}
356+
return secrets, nil
357+
}
358+
359+
func fillTemplatedSecretData(dbcr *kciv1alpha1.Database, secretData map[string][]byte, newSecretFields map[string]string) (newSecret *v1.Secret) {
360+
blockedTempatedKeys := getBlockedTempatedKeys()
361+
for key, value := range newSecretFields {
362+
if slices.Contains(blockedTempatedKeys, key) {
363+
logrus.Warnf("DB: namespace=%s, name=%s %s can't be used for templating, because it's used for default secret created by operator",
364+
dbcr.Namespace,
365+
dbcr.Name,
366+
key,
367+
)
368+
} else {
369+
newSecret = addTemplatedSecretToSecret(dbcr, secretData, key, value)
370+
}
371+
}
372+
return
373+
}
374+
265375
func addConnectionStringToSecret(dbcr *kciv1alpha1.Database, secretData map[string][]byte, connectionString string) *v1.Secret {
266376
secretData["CONNECTION_STRING"] = []byte(connectionString)
267377
return kci.SecretBuilder(dbcr.Spec.SecretName, dbcr.GetNamespace(), secretData)
268378
}
379+
380+
func addTemplatedSecretToSecret(dbcr *kciv1alpha1.Database, secretData map[string][]byte, secretName string, secretValue string) *v1.Secret {
381+
secretData[secretName] = []byte(secretValue)
382+
return kci.SecretBuilder(dbcr.Spec.SecretName, dbcr.GetNamespace(), secretData)
383+
}
384+
385+
func removeObsoleteSecret(dbcr *kciv1alpha1.Database, secretData map[string][]byte, newSecretFields map[string]string) *v1.Secret {
386+
blockedTempatedKeys := getBlockedTempatedKeys()
387+
388+
for key := range secretData {
389+
if _, ok := newSecretFields[key]; !ok {
390+
// Check if is a untemplatead secret, so it's not removed accidentally
391+
if !slices.Contains(blockedTempatedKeys, key) {
392+
logrus.Infof("DB: namespace=%s, name=%s removing an obsolete field: %s", dbcr.Namespace, dbcr.Name, key)
393+
delete(secretData, key)
394+
}
395+
}
396+
}
397+
398+
return kci.SecretBuilder(dbcr.Spec.SecretName, dbcr.GetNamespace(), secretData)
399+
}

0 commit comments

Comments
 (0)