Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
61 changes: 60 additions & 1 deletion docs/administrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,14 @@ spec:

## Custom Pod Environment Variables

### ConfigMap

It is possible to configure a ConfigMap which is used by the Postgres pods as
an additional provider for environment variables.

One use case is to customize the Spilo image and configure it with environment
variables. The ConfigMap with the additional settings is configured in the
operator's main ConfigMap:
operator's main ConfigMap or CRD OperatorConfiguration:

**postgres-operator ConfigMap**

Expand All @@ -255,6 +257,19 @@ data:
...
```

**OperatorConfiguration CRD**

```yaml
apiVersion: "acid.zalan.do/v1"
kind: OperatorConfiguration
metadata:
name: postgresql-operator-default-configuration
configuration:
# referencing config map with custom settings
pod_environment_configmap: postgres-pod-config
...
```

**referenced ConfigMap `postgres-pod-config`**

```yaml
Expand All @@ -269,6 +284,50 @@ data:

This ConfigMap is then added as a source of environment variables to the
Postgres StatefulSet/pods.
Changes in the original ConfigMap will be reflected in pods environment after
periodic resync (performed every `resync_period` seconds).

### Secret

It is possible to configure a Secret which is used by the Postgres pods as
an additional provider for environment variables.

One use case is to customize the Spilo image and configure it with environment
variables like `AWS_SECRET_ACCESS_KEY`. The Secret with the additional
settings is configured in the operator's CRD OperatorConfiguration:

**OperatorConfiguration CRD**

```yaml
apiVersion: "acid.zalan.do/v1"
kind: OperatorConfiguration
metadata:
name: postgresql-operator-default-configuration
configuration:
...
# referencing config map with custom settings
pod_environment_secret_name: postgres-pod-secret
pod_environment_secret_keys:
minio_secret: AWS_SECRET_ACCESS_KEY
...
```

**referenced Secret `postgres-pod-secret`**

```yaml
apiVersion: v1
kind: Secret
metadata:
name: postgres-pod-secret
namespace: default
data:
minio_secret: <base64_secret_value>
```

This Secret keys are then added as a source of environment variables to the
Postgres StatefulSet/pods.
Changes in the Secret data will be reflected in pods environments after
periodic resync (performed every `resync_period` seconds).

## Limiting the number of instances in clusters with `min_instances` and `max_instances`

Expand Down
14 changes: 14 additions & 0 deletions docs/reference/operator_parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,20 @@ configuration they are grouped under the `kubernetes` key.
conflicts they are overridden by the environment variables generated by the
operator. The default is empty.

* **pod_environment_secret_name**
a name of the Secret with environment variables to populate on every pod.
The Secret should be present in the namespace of the postgres cluster.
[References](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.12/#secretkeyselector-v1-core)
to all keys from that Secret are injected to the pod's environment, name of
the variable is the same as the secret's key. On conflicts they are
overridden by the environment variables generated by the operator.
The `pod_environment_secret_keys` option can be used to choose keys to
populate and change environment variables names. The default is empty.

* **pod_environment_secret_keys**
an object to specify which keys in the `pod_environment_secret_name` Secret should
populate pod environment. The values are environment variable names. Optional.

* **pod_priority_class_name**
a name of the [priority
class](https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass)
Expand Down
12 changes: 7 additions & 5 deletions pkg/apis/acid.zalan.do/v1/operator_configuration_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@ type KubernetesMetaConfiguration struct {
// TODO: use a proper toleration structure?
PodToleration map[string]string `json:"toleration,omitempty"`
// TODO: use namespacedname
PodEnvironmentConfigMap string `json:"pod_environment_configmap,omitempty"`
PodPriorityClassName string `json:"pod_priority_class_name,omitempty"`
MasterPodMoveTimeout time.Duration `json:"master_pod_move_timeout,omitempty"`
EnablePodAntiAffinity bool `json:"enable_pod_antiaffinity" default:"false"`
PodAntiAffinityTopologyKey string `name:"pod_antiaffinity_topology_key" default:"kubernetes.io/hostname"`
PodEnvironmentConfigMap string `json:"pod_environment_configmap,omitempty"`
PodEnvironmentSecretName string `json:"pod_environment_secret_name,omitempty"`
PodEnvironmentSecretKeys map[string]string `json:"pod_environment_secret_keys,omitempty"`
PodPriorityClassName string `json:"pod_priority_class_name,omitempty"`
MasterPodMoveTimeout time.Duration `json:"master_pod_move_timeout,omitempty"`
EnablePodAntiAffinity bool `json:"enable_pod_antiaffinity" default:"false"`
PodAntiAffinityTopologyKey string `name:"pod_antiaffinity_topology_key" default:"kubernetes.io/hostname"`
}

// PostgresPodResourcesDefaults defines the spec of default resources
Expand Down
7 changes: 7 additions & 0 deletions pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go

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

132 changes: 117 additions & 15 deletions pkg/cluster/k8sres.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package cluster

import (
"crypto/sha256"
"encoding/json"
"fmt"
"hash"
"sort"

"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -86,6 +88,94 @@ func (c *Cluster) podDisruptionBudgetName() string {
return c.OpConfig.PDBNameFormat.Format("cluster", c.Name)
}

func (c *Cluster) getPodEnvironmentConfigMapVariables() ([]v1.EnvVar, error) {
vars := make([]v1.EnvVar, 0)

if c.OpConfig.PodEnvironmentConfigMap == "" {
return vars, nil
}

cm, err := c.KubeClient.ConfigMaps(c.Namespace).Get(c.OpConfig.PodEnvironmentConfigMap, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("could not read ConfigMap PodEnvironmentConfigMap: %v", err)
}
for k, v := range cm.Data {
vars = append(vars, v1.EnvVar{Name: k, Value: v})
}

return vars, nil
}

func hashUpdate(prevHash hash.Hash, val []byte) hash.Hash {
var newHash hash.Hash
if prevHash != nil {
newHash = prevHash
} else {
newHash = sha256.New()
}
_, _ = newHash.Write(val)
_, _ = newHash.Write([]byte("\x00")) // NUL-byte separator
return newHash
}

func (c *Cluster) getPodEnvironmentSecretVariables() ([]v1.EnvVar, []byte, error) {
vars := make([]v1.EnvVar, 0)

if c.OpConfig.PodEnvironmentSecretName == "" {
return vars, nil, nil
}

secret, err := c.KubeClient.Secrets(c.Namespace).Get(c.OpConfig.PodEnvironmentSecretName, metav1.GetOptions{})
if err != nil {
return nil, nil, fmt.Errorf("could not read Secret PodEnvironmentSecretName: %v", err)
}

sortedKeys := make([]string, 0)
keyToEnvName := make(map[string]string)
if len(c.OpConfig.PodEnvironmentSecretKeys) > 0 {
for k, v := range c.OpConfig.PodEnvironmentSecretKeys {
_, ok := secret.Data[k]
if !ok {
return nil, nil, fmt.Errorf("could not read Secret key %s (present in PodEnvironmentSecretKeys)", k)
}
sortedKeys = append(sortedKeys, k)
keyToEnvName[k] = v
}
} else {
for k := range secret.Data {
sortedKeys = append(sortedKeys, k)
keyToEnvName[k] = k
}
}
sort.Strings(sortedKeys)

followHash := hashUpdate(nil, []byte(c.OpConfig.PodEnvironmentSecretName))

for _, keyName := range sortedKeys {
vars = append(
vars,
v1.EnvVar{
Name: keyToEnvName[keyName],
ValueFrom: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: c.OpConfig.PodEnvironmentSecretName,
},
Key: keyName,
},
},
},
)

// update hash with environment variable name and secret value
// secret key name don't affect Pod (and is already in SecretKeyRef)
followHash = hashUpdate(followHash, []byte(keyToEnvName[keyName]))
followHash = hashUpdate(followHash, secret.Data[keyName])
}

return vars, followHash.Sum(nil), nil
}

func (c *Cluster) makeDefaultResources() acidv1.Resources {

config := c.OpConfig
Expand Down Expand Up @@ -429,6 +519,7 @@ func mountShmVolumeNeeded(opConfig config.Config, pgSpec *acidv1.PostgresSpec) b
func generatePodTemplate(
namespace string,
labels labels.Set,
annotations map[string]string,
spiloContainer *v1.Container,
initContainers []v1.Container,
sidecarContainers []v1.Container,
Expand Down Expand Up @@ -471,13 +562,14 @@ func generatePodTemplate(

template := v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
Namespace: namespace,
Labels: labels,
Namespace: namespace,
Annotations: annotations,
},
Spec: podSpec,
}
if kubeIAMRole != "" {
template.Annotations = map[string]string{constants.KubeIAmAnnotation: kubeIAMRole}
template.Annotations[constants.KubeIAmAnnotation] = kubeIAMRole
}

return &template, nil
Expand Down Expand Up @@ -763,21 +855,22 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*v1beta1.State
return nil, fmt.Errorf("could not generate resource requirements: %v", err)
}

customPodEnvVarsList := make([]v1.EnvVar, 0)
configMapEnvVarsList, err := c.getPodEnvironmentConfigMapVariables()
if err != nil {
return nil, err
}

if c.OpConfig.PodEnvironmentConfigMap != "" {
var cm *v1.ConfigMap
cm, err = c.KubeClient.ConfigMaps(c.Namespace).Get(c.OpConfig.PodEnvironmentConfigMap, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("could not read PodEnvironmentConfigMap: %v", err)
}
for k, v := range cm.Data {
customPodEnvVarsList = append(customPodEnvVarsList, v1.EnvVar{Name: k, Value: v})
}
sort.Slice(customPodEnvVarsList,
func(i, j int) bool { return customPodEnvVarsList[i].Name < customPodEnvVarsList[j].Name })
secretEnvVarsList, secretEnvVarsHash, err := c.getPodEnvironmentSecretVariables()
if err != nil {
return nil, err
}

customPodEnvVarsList := append(configMapEnvVarsList, secretEnvVarsList...)

// Don't reorder variables - avoid Pod restarts
sort.Slice(customPodEnvVarsList,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why sorting ? to simplify displaying ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved it moved from:

sort.Slice(customPodEnvVarsList,
func(i, j int) bool { return customPodEnvVarsList[i].Name < customPodEnvVarsList[j].Name })

That's because the customPodEnvVarsList is being appended not only by PodEnvironmentConfigMap code but also PodEnvironmentSecretName.

func(i, j int) bool { return customPodEnvVarsList[i].Name < customPodEnvVarsList[j].Name })

spiloConfiguration := generateSpiloJSONConfiguration(&spec.PostgresqlParam, &spec.Patroni, c.OpConfig.PamRoleName, c.logger)

// generate environment variables for the spilo container
Expand Down Expand Up @@ -828,10 +921,19 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*v1beta1.State
tolerationSpec := tolerations(&spec.Tolerations, c.OpConfig.PodToleration)
effectivePodPriorityClassName := util.Coalesce(spec.PodPriorityClassName, c.OpConfig.PodPriorityClassName)

var customPodAnnotations map[string]string
if secretEnvVarsHash != nil {
secretEnvVarsAnnKey := fmt.Sprintf(constants.PodEnvironmentSecretFollowAnnotationFmt,
c.OpConfig.PodEnvironmentSecretName)
customPodAnnotations = make(map[string]string)
customPodAnnotations[secretEnvVarsAnnKey] = fmt.Sprintf("%x", secretEnvVarsHash)
}

// generate pod template for the statefulset, based on the spilo container and sidecars
if podTemplate, err = generatePodTemplate(
c.Namespace,
c.labelsSet(true),
customPodAnnotations,
spiloContainer,
spec.InitContainers,
sidecarContainers,
Expand Down
Loading