Skip to content

Commit ece341d

Browse files
authored
Allow pod environment variables to also be sourced from a secret (#946)
* Extend operator configuration to allow for a pod_environment_secret just like pod_environment_configmap * Add all keys from PodEnvironmentSecrets as ENV vars (using SecretKeyRef to protect the value) * Apply envVars from pod_environment_configmap and pod_environment_secrets before doing the global settings from the operator config. This allows them to be overriden by the user (via configmap / secret) * Add ability use a Secret for custom pod envVars (via pod_environment_secret) to admin documentation * Add pod_environment_secret to Helm chart values.yaml * Add unit tests for PodEnvironmentConfigMap and PodEnvironmentSecret - highly inspired by @kupson and his very similar PR #481 * Added new parameter pod_environment_secret to operatorconfig CRD and configmap examples * Add pod_environment_secret to the operationconfiguration CRD Co-authored-by: Christian Rohmann <[email protected]>
1 parent 102a353 commit ece341d

13 files changed

+393
-54
lines changed

charts/postgres-operator/crds/operatorconfigurations.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ spec:
149149
type: string
150150
pod_environment_configmap:
151151
type: string
152+
pod_environment_secret:
153+
type: string
152154
pod_management_policy:
153155
type: string
154156
enum:

charts/postgres-operator/values-crd.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ configKubernetes:
104104
pod_antiaffinity_topology_key: "kubernetes.io/hostname"
105105
# namespaced name of the ConfigMap with environment variables to populate on every pod
106106
# pod_environment_configmap: "default/my-custom-config"
107+
# name of the Secret (in cluster namespace) with environment variables to populate on every pod
108+
# pod_environment_secret: "my-custom-secret"
107109

108110
# specify the pod management policy of stateful sets of Postgres clusters
109111
pod_management_policy: "ordered_ready"

charts/postgres-operator/values.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ configKubernetes:
9595
pod_antiaffinity_topology_key: "kubernetes.io/hostname"
9696
# namespaced name of the ConfigMap with environment variables to populate on every pod
9797
# pod_environment_configmap: "default/my-custom-config"
98+
# name of the Secret (in cluster namespace) with environment variables to populate on every pod
99+
# pod_environment_secret: "my-custom-secret"
98100

99101
# specify the pod management policy of stateful sets of Postgres clusters
100102
pod_management_policy: "ordered_ready"

docs/administrator.md

+59-5
Original file line numberDiff line numberDiff line change
@@ -319,11 +319,18 @@ spec:
319319

320320

321321
## Custom Pod Environment Variables
322-
323-
It is possible to configure a ConfigMap which is used by the Postgres pods as
322+
It is possible to configure a ConfigMap as well as a Secret which are used by the Postgres pods as
324323
an additional provider for environment variables. One use case is to customize
325-
the Spilo image and configure it with environment variables. The ConfigMap with
326-
the additional settings is referenced in the operator's main configuration.
324+
the Spilo image and configure it with environment variables. Another case could be to provide custom
325+
cloud provider or backup settings.
326+
327+
In general the Operator will give preference to the globally configured variables, to not have the custom
328+
ones interfere with core functionality. Variables with the 'WAL_' and 'LOG_' prefix can be overwritten though, to allow
329+
backup and logshipping to be specified differently.
330+
331+
332+
### Via ConfigMap
333+
The ConfigMap with the additional settings is referenced in the operator's main configuration.
327334
A namespace can be specified along with the name. If left out, the configured
328335
default namespace of your K8s client will be used and if the ConfigMap is not
329336
found there, the Postgres cluster's namespace is taken when different:
@@ -365,7 +372,54 @@ data:
365372
MY_CUSTOM_VAR: value
366373
```
367374

368-
This ConfigMap is then added as a source of environment variables to the
375+
The key-value pairs of the ConfigMap are then added as environment variables to the
376+
Postgres StatefulSet/pods.
377+
378+
379+
### Via Secret
380+
The Secret with the additional variables is referenced in the operator's main configuration.
381+
To protect the values of the secret from being exposed in the pod spec they are each referenced
382+
as SecretKeyRef.
383+
This does not allow for the secret to be in a different namespace as the pods though
384+
385+
**postgres-operator ConfigMap**
386+
387+
```yaml
388+
apiVersion: v1
389+
kind: ConfigMap
390+
metadata:
391+
name: postgres-operator
392+
data:
393+
# referencing secret with custom environment variables
394+
pod_environment_secret: postgres-pod-secrets
395+
```
396+
397+
**OperatorConfiguration**
398+
399+
```yaml
400+
apiVersion: "acid.zalan.do/v1"
401+
kind: OperatorConfiguration
402+
metadata:
403+
name: postgresql-operator-configuration
404+
configuration:
405+
kubernetes:
406+
# referencing secret with custom environment variables
407+
pod_environment_secret: postgres-pod-secrets
408+
```
409+
410+
**referenced Secret `postgres-pod-secrets`**
411+
412+
```yaml
413+
apiVersion: v1
414+
kind: Secret
415+
metadata:
416+
name: postgres-pod-secrets
417+
namespace: default
418+
data:
419+
MY_CUSTOM_VAR: dmFsdWU=
420+
```
421+
422+
The key-value pairs of the Secret are all accessible as environment variables to the
369423
Postgres StatefulSet/pods.
370424

371425
## Limiting the number of min and max instances in clusters

manifests/configmap.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ data:
7474
# pod_antiaffinity_topology_key: "kubernetes.io/hostname"
7575
pod_deletion_wait_timeout: 10m
7676
# pod_environment_configmap: "default/my-custom-config"
77+
# pod_environment_secret: "my-custom-secret"
7778
pod_label_wait_timeout: 10m
7879
pod_management_policy: "ordered_ready"
7980
pod_role_label: spilo-role

manifests/operatorconfiguration.crd.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ spec:
145145
type: string
146146
pod_environment_configmap:
147147
type: string
148+
pod_environment_secret:
149+
type: string
148150
pod_management_policy:
149151
type: string
150152
enum:

manifests/postgresql-operator-default-configuration.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ configuration:
4949
pdb_name_format: "postgres-{cluster}-pdb"
5050
pod_antiaffinity_topology_key: "kubernetes.io/hostname"
5151
# pod_environment_configmap: "default/my-custom-config"
52+
# pod_environment_secret: "my-custom-secret"
5253
pod_management_policy: "ordered_ready"
5354
# pod_priority_class_name: ""
5455
pod_role_label: spilo-role

pkg/apis/acid.zalan.do/v1/crds.go

+3
Original file line numberDiff line numberDiff line change
@@ -942,6 +942,9 @@ var OperatorConfigCRDResourceValidation = apiextv1beta1.CustomResourceValidation
942942
"pod_environment_configmap": {
943943
Type: "string",
944944
},
945+
"pod_environment_secret": {
946+
Type: "string",
947+
},
945948
"pod_management_policy": {
946949
Type: "string",
947950
Enum: []apiextv1beta1.JSON{

pkg/apis/acid.zalan.do/v1/operator_configuration_type.go

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ type KubernetesMetaConfiguration struct {
7070
// TODO: use a proper toleration structure?
7171
PodToleration map[string]string `json:"toleration,omitempty"`
7272
PodEnvironmentConfigMap spec.NamespacedName `json:"pod_environment_configmap,omitempty"`
73+
PodEnvironmentSecret string `json:"pod_environment_secret,omitempty"`
7374
PodPriorityClassName string `json:"pod_priority_class_name,omitempty"`
7475
MasterPodMoveTimeout Duration `json:"master_pod_move_timeout,omitempty"`
7576
EnablePodAntiAffinity bool `json:"enable_pod_antiaffinity,omitempty"`

pkg/cluster/k8sres.go

+110-49
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"path"
88
"sort"
99
"strconv"
10+
"strings"
1011

1112
"github.com/sirupsen/logrus"
1213

@@ -20,7 +21,6 @@ import (
2021

2122
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
2223
"github.com/zalando/postgres-operator/pkg/spec"
23-
pkgspec "github.com/zalando/postgres-operator/pkg/spec"
2424
"github.com/zalando/postgres-operator/pkg/util"
2525
"github.com/zalando/postgres-operator/pkg/util/config"
2626
"github.com/zalando/postgres-operator/pkg/util/constants"
@@ -715,28 +715,6 @@ func (c *Cluster) generateSpiloPodEnvVars(uid types.UID, spiloConfiguration stri
715715
envVars = append(envVars, v1.EnvVar{Name: "SPILO_CONFIGURATION", Value: spiloConfiguration})
716716
}
717717

718-
if c.OpConfig.WALES3Bucket != "" {
719-
envVars = append(envVars, v1.EnvVar{Name: "WAL_S3_BUCKET", Value: c.OpConfig.WALES3Bucket})
720-
envVars = append(envVars, v1.EnvVar{Name: "WAL_BUCKET_SCOPE_SUFFIX", Value: getBucketScopeSuffix(string(uid))})
721-
envVars = append(envVars, v1.EnvVar{Name: "WAL_BUCKET_SCOPE_PREFIX", Value: ""})
722-
}
723-
724-
if c.OpConfig.WALGSBucket != "" {
725-
envVars = append(envVars, v1.EnvVar{Name: "WAL_GS_BUCKET", Value: c.OpConfig.WALGSBucket})
726-
envVars = append(envVars, v1.EnvVar{Name: "WAL_BUCKET_SCOPE_SUFFIX", Value: getBucketScopeSuffix(string(uid))})
727-
envVars = append(envVars, v1.EnvVar{Name: "WAL_BUCKET_SCOPE_PREFIX", Value: ""})
728-
}
729-
730-
if c.OpConfig.GCPCredentials != "" {
731-
envVars = append(envVars, v1.EnvVar{Name: "GOOGLE_APPLICATION_CREDENTIALS", Value: c.OpConfig.GCPCredentials})
732-
}
733-
734-
if c.OpConfig.LogS3Bucket != "" {
735-
envVars = append(envVars, v1.EnvVar{Name: "LOG_S3_BUCKET", Value: c.OpConfig.LogS3Bucket})
736-
envVars = append(envVars, v1.EnvVar{Name: "LOG_BUCKET_SCOPE_SUFFIX", Value: getBucketScopeSuffix(string(uid))})
737-
envVars = append(envVars, v1.EnvVar{Name: "LOG_BUCKET_SCOPE_PREFIX", Value: ""})
738-
}
739-
740718
if c.patroniUsesKubernetes() {
741719
envVars = append(envVars, v1.EnvVar{Name: "DCS_ENABLE_KUBERNETES_API", Value: "true"})
742720
} else {
@@ -755,10 +733,34 @@ func (c *Cluster) generateSpiloPodEnvVars(uid types.UID, spiloConfiguration stri
755733
envVars = append(envVars, c.generateStandbyEnvironment(standbyDescription)...)
756734
}
757735

736+
// add vars taken from pod_environment_configmap and pod_environment_secret first
737+
// (to allow them to override the globals set in the operator config)
758738
if len(customPodEnvVarsList) > 0 {
759739
envVars = append(envVars, customPodEnvVarsList...)
760740
}
761741

742+
if c.OpConfig.WALES3Bucket != "" {
743+
envVars = append(envVars, v1.EnvVar{Name: "WAL_S3_BUCKET", Value: c.OpConfig.WALES3Bucket})
744+
envVars = append(envVars, v1.EnvVar{Name: "WAL_BUCKET_SCOPE_SUFFIX", Value: getBucketScopeSuffix(string(uid))})
745+
envVars = append(envVars, v1.EnvVar{Name: "WAL_BUCKET_SCOPE_PREFIX", Value: ""})
746+
}
747+
748+
if c.OpConfig.WALGSBucket != "" {
749+
envVars = append(envVars, v1.EnvVar{Name: "WAL_GS_BUCKET", Value: c.OpConfig.WALGSBucket})
750+
envVars = append(envVars, v1.EnvVar{Name: "WAL_BUCKET_SCOPE_SUFFIX", Value: getBucketScopeSuffix(string(uid))})
751+
envVars = append(envVars, v1.EnvVar{Name: "WAL_BUCKET_SCOPE_PREFIX", Value: ""})
752+
}
753+
754+
if c.OpConfig.GCPCredentials != "" {
755+
envVars = append(envVars, v1.EnvVar{Name: "GOOGLE_APPLICATION_CREDENTIALS", Value: c.OpConfig.GCPCredentials})
756+
}
757+
758+
if c.OpConfig.LogS3Bucket != "" {
759+
envVars = append(envVars, v1.EnvVar{Name: "LOG_S3_BUCKET", Value: c.OpConfig.LogS3Bucket})
760+
envVars = append(envVars, v1.EnvVar{Name: "LOG_BUCKET_SCOPE_SUFFIX", Value: getBucketScopeSuffix(string(uid))})
761+
envVars = append(envVars, v1.EnvVar{Name: "LOG_BUCKET_SCOPE_PREFIX", Value: ""})
762+
}
763+
762764
return envVars
763765
}
764766

@@ -777,13 +779,81 @@ func deduplicateEnvVars(input []v1.EnvVar, containerName string, logger *logrus.
777779
result = append(result, input[i])
778780
} else if names[va.Name] == 1 {
779781
names[va.Name]++
780-
logger.Warningf("variable %q is defined in %q more than once, the subsequent definitions are ignored",
781-
va.Name, containerName)
782+
783+
// Some variables (those to configure the WAL_ and LOG_ shipping) may be overriden, only log as info
784+
if strings.HasPrefix(va.Name, "WAL_") || strings.HasPrefix(va.Name, "LOG_") {
785+
logger.Infof("global variable %q has been overwritten by configmap/secret for container %q",
786+
va.Name, containerName)
787+
} else {
788+
logger.Warningf("variable %q is defined in %q more than once, the subsequent definitions are ignored",
789+
va.Name, containerName)
790+
}
782791
}
783792
}
784793
return result
785794
}
786795

796+
// Return list of variables the pod recieved from the configured ConfigMap
797+
func (c *Cluster) getPodEnvironmentConfigMapVariables() ([]v1.EnvVar, error) {
798+
configMapPodEnvVarsList := make([]v1.EnvVar, 0)
799+
800+
if c.OpConfig.PodEnvironmentConfigMap.Name == "" {
801+
return configMapPodEnvVarsList, nil
802+
}
803+
804+
cm, err := c.KubeClient.ConfigMaps(c.OpConfig.PodEnvironmentConfigMap.Namespace).Get(
805+
context.TODO(),
806+
c.OpConfig.PodEnvironmentConfigMap.Name,
807+
metav1.GetOptions{})
808+
if err != nil {
809+
// if not found, try again using the cluster's namespace if it's different (old behavior)
810+
if k8sutil.ResourceNotFound(err) && c.Namespace != c.OpConfig.PodEnvironmentConfigMap.Namespace {
811+
cm, err = c.KubeClient.ConfigMaps(c.Namespace).Get(
812+
context.TODO(),
813+
c.OpConfig.PodEnvironmentConfigMap.Name,
814+
metav1.GetOptions{})
815+
}
816+
if err != nil {
817+
return nil, fmt.Errorf("could not read PodEnvironmentConfigMap: %v", err)
818+
}
819+
}
820+
for k, v := range cm.Data {
821+
configMapPodEnvVarsList = append(configMapPodEnvVarsList, v1.EnvVar{Name: k, Value: v})
822+
}
823+
return configMapPodEnvVarsList, nil
824+
}
825+
826+
// Return list of variables the pod recieved from the configured Secret
827+
func (c *Cluster) getPodEnvironmentSecretVariables() ([]v1.EnvVar, error) {
828+
secretPodEnvVarsList := make([]v1.EnvVar, 0)
829+
830+
if c.OpConfig.PodEnvironmentSecret == "" {
831+
return secretPodEnvVarsList, nil
832+
}
833+
834+
secret, err := c.KubeClient.Secrets(c.OpConfig.PodEnvironmentSecret).Get(
835+
context.TODO(),
836+
c.OpConfig.PodEnvironmentSecret,
837+
metav1.GetOptions{})
838+
if err != nil {
839+
return nil, fmt.Errorf("could not read Secret PodEnvironmentSecretName: %v", err)
840+
}
841+
842+
for k := range secret.Data {
843+
secretPodEnvVarsList = append(secretPodEnvVarsList,
844+
v1.EnvVar{Name: k, ValueFrom: &v1.EnvVarSource{
845+
SecretKeyRef: &v1.SecretKeySelector{
846+
LocalObjectReference: v1.LocalObjectReference{
847+
Name: c.OpConfig.PodEnvironmentSecret,
848+
},
849+
Key: k,
850+
},
851+
}})
852+
}
853+
854+
return secretPodEnvVarsList, nil
855+
}
856+
787857
func getSidecarContainer(sidecar acidv1.Sidecar, index int, resources *v1.ResourceRequirements) *v1.Container {
788858
name := sidecar.Name
789859
if name == "" {
@@ -943,32 +1013,23 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
9431013
initContainers = spec.InitContainers
9441014
}
9451015

946-
customPodEnvVarsList := make([]v1.EnvVar, 0)
1016+
// fetch env vars from custom ConfigMap
1017+
configMapEnvVarsList, err := c.getPodEnvironmentConfigMapVariables()
1018+
if err != nil {
1019+
return nil, err
1020+
}
9471021

948-
if c.OpConfig.PodEnvironmentConfigMap != (pkgspec.NamespacedName{}) {
949-
var cm *v1.ConfigMap
950-
cm, err = c.KubeClient.ConfigMaps(c.OpConfig.PodEnvironmentConfigMap.Namespace).Get(
951-
context.TODO(),
952-
c.OpConfig.PodEnvironmentConfigMap.Name,
953-
metav1.GetOptions{})
954-
if err != nil {
955-
// if not found, try again using the cluster's namespace if it's different (old behavior)
956-
if k8sutil.ResourceNotFound(err) && c.Namespace != c.OpConfig.PodEnvironmentConfigMap.Namespace {
957-
cm, err = c.KubeClient.ConfigMaps(c.Namespace).Get(
958-
context.TODO(),
959-
c.OpConfig.PodEnvironmentConfigMap.Name,
960-
metav1.GetOptions{})
961-
}
962-
if err != nil {
963-
return nil, fmt.Errorf("could not read PodEnvironmentConfigMap: %v", err)
964-
}
965-
}
966-
for k, v := range cm.Data {
967-
customPodEnvVarsList = append(customPodEnvVarsList, v1.EnvVar{Name: k, Value: v})
968-
}
969-
sort.Slice(customPodEnvVarsList,
970-
func(i, j int) bool { return customPodEnvVarsList[i].Name < customPodEnvVarsList[j].Name })
1022+
// fetch env vars from custom ConfigMap
1023+
secretEnvVarsList, err := c.getPodEnvironmentSecretVariables()
1024+
if err != nil {
1025+
return nil, err
9711026
}
1027+
1028+
// concat all custom pod env vars and sort them
1029+
customPodEnvVarsList := append(configMapEnvVarsList, secretEnvVarsList...)
1030+
sort.Slice(customPodEnvVarsList,
1031+
func(i, j int) bool { return customPodEnvVarsList[i].Name < customPodEnvVarsList[j].Name })
1032+
9721033
if spec.StandbyCluster != nil && spec.StandbyCluster.S3WalPath == "" {
9731034
return nil, fmt.Errorf("s3_wal_path is empty for standby cluster")
9741035
}

0 commit comments

Comments
 (0)