Merge branch 'master' into bump-v1.5.0

This commit is contained in:
Felix Kunde 2020-05-04 16:25:10 +02:00
commit 2a38091cc3
21 changed files with 244 additions and 62 deletions

View File

@ -117,6 +117,10 @@ spec:
type: object type: object
additionalProperties: additionalProperties:
type: string type: string
downscaler_annotations:
type: array
items:
type: string
enable_init_containers: enable_init_containers:
type: boolean type: boolean
enable_pod_antiaffinity: enable_pod_antiaffinity:

View File

@ -67,6 +67,11 @@ configKubernetes:
# keya: valuea # keya: valuea
# keyb: valueb # keyb: valueb
# list of annotations propagated from cluster manifest to statefulset and deployment
# downscaler_annotations:
# - deployment-time
# - downscaler/*
# enables initContainers to run actions before Spilo is started # enables initContainers to run actions before Spilo is started
enable_init_containers: true enable_init_containers: true
# toggles pod anti affinity on the Postgres pods # toggles pod anti affinity on the Postgres pods
@ -262,11 +267,11 @@ configConnectionPooler:
# docker image # docker image
connection_pooler_image: "registry.opensource.zalan.do/acid/pgbouncer" connection_pooler_image: "registry.opensource.zalan.do/acid/pgbouncer"
# max db connections the pooler should hold # max db connections the pooler should hold
connection_pooler_max_db_connections: "60" connection_pooler_max_db_connections: 60
# default pooling mode # default pooling mode
connection_pooler_mode: "transaction" connection_pooler_mode: "transaction"
# number of pooler instances # number of pooler instances
connection_pooler_number_of_instances: "2" connection_pooler_number_of_instances: 2
# default resources # default resources
connection_pooler_default_cpu_request: 500m connection_pooler_default_cpu_request: 500m
connection_pooler_default_memory_request: 100Mi connection_pooler_default_memory_request: 100Mi

View File

@ -63,6 +63,9 @@ configKubernetes:
# annotations attached to each database pod # annotations attached to each database pod
# custom_pod_annotations: "keya:valuea,keyb:valueb" # custom_pod_annotations: "keya:valuea,keyb:valueb"
# list of annotations propagated from cluster manifest to statefulset and deployment
# downscaler_annotations: "deployment-time,downscaler/*"
# enables initContainers to run actions before Spilo is started # enables initContainers to run actions before Spilo is started
enable_init_containers: "true" enable_init_containers: "true"
# toggles pod anti affinity on the Postgres pods # toggles pod anti affinity on the Postgres pods
@ -106,7 +109,7 @@ configKubernetes:
# Postgres pods are terminated forcefully after this timeout # Postgres pods are terminated forcefully after this timeout
pod_terminate_grace_period: 5m pod_terminate_grace_period: 5m
# template for database user secrets generated by the operator # template for database user secrets generated by the operator
secret_name_template: '{username}.{cluster}.credentials' secret_name_template: "{username}.{cluster}.credentials.{tprkind}.{tprgroup}"
# group ID with write-access to volumes (required to run Spilo as non-root process) # group ID with write-access to volumes (required to run Spilo as non-root process)
# spilo_fsgroup: "103" # spilo_fsgroup: "103"

View File

@ -165,7 +165,7 @@ These parameters are grouped directly under the `spec` key in the manifest.
If `targetContainers` is empty, additional volumes will be mounted only in the `postgres` container. If `targetContainers` is empty, additional volumes will be mounted only in the `postgres` container.
If you set the `all` special item, it will be mounted in all containers (postgres + sidecars). If you set the `all` special item, it will be mounted in all containers (postgres + sidecars).
Else you can set the list of target containers in which the additional volumes will be mounted (eg : postgres, telegraf) Else you can set the list of target containers in which the additional volumes will be mounted (eg : postgres, telegraf)
## Postgres parameters ## Postgres parameters
Those parameters are grouped under the `postgresql` top-level key, which is Those parameters are grouped under the `postgresql` top-level key, which is

View File

@ -200,6 +200,12 @@ configuration they are grouped under the `kubernetes` key.
of a database created by the operator. If the annotation key is also provided of a database created by the operator. If the annotation key is also provided
by the database definition, the database definition value is used. by the database definition, the database definition value is used.
* **downscaler_annotations**
An array of annotations that should be passed from Postgres CRD on to the
statefulset and, if exists, to the connection pooler deployment as well.
Regular expressions like `downscaler/*` etc. are also accepted. Can be used
with [kube-downscaler](https://github.com/hjacobs/kube-downscaler).
* **watched_namespace** * **watched_namespace**
The operator watches for Postgres objects in the given namespace. If not The operator watches for Postgres objects in the given namespace. If not
specified, the value is taken from the operator namespace. A special `*` specified, the value is taken from the operator namespace. A special `*`

View File

@ -428,7 +428,7 @@ class EndToEndTestCase(unittest.TestCase):
k8s.api.core_v1.patch_node(current_master_node, patch_readiness_label) k8s.api.core_v1.patch_node(current_master_node, patch_readiness_label)
# wait a little before proceeding with the pod distribution test # wait a little before proceeding with the pod distribution test
time.sleep(k8s.RETRY_TIMEOUT_SEC) time.sleep(30)
# toggle pod anti affinity to move replica away from master node # toggle pod anti affinity to move replica away from master node
self.assert_distributed_pods(new_master_node, new_replica_nodes, cluster_label) self.assert_distributed_pods(new_master_node, new_replica_nodes, cluster_label)
@ -465,21 +465,24 @@ class EndToEndTestCase(unittest.TestCase):
pg_patch_custom_annotations = { pg_patch_custom_annotations = {
"spec": { "spec": {
"serviceAnnotations": { "serviceAnnotations": {
"annotation.key": "value" "annotation.key": "value",
"foo": "bar",
} }
} }
} }
k8s.api.custom_objects_api.patch_namespaced_custom_object( k8s.api.custom_objects_api.patch_namespaced_custom_object(
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_custom_annotations) "acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_custom_annotations)
# wait a little before proceeding
time.sleep(30)
annotations = { annotations = {
"annotation.key": "value", "annotation.key": "value",
"foo": "bar", "foo": "bar",
} }
self.assertTrue(k8s.check_service_annotations( self.assertTrue(k8s.check_service_annotations(
"cluster-name=acid-service-annotations,spilo-role=master", annotations)) "cluster-name=acid-minimal-cluster,spilo-role=master", annotations))
self.assertTrue(k8s.check_service_annotations( self.assertTrue(k8s.check_service_annotations(
"cluster-name=acid-service-annotations,spilo-role=replica", annotations)) "cluster-name=acid-minimal-cluster,spilo-role=replica", annotations))
# clean up # clean up
unpatch_custom_service_annotations = { unpatch_custom_service_annotations = {
@ -489,6 +492,40 @@ class EndToEndTestCase(unittest.TestCase):
} }
k8s.update_config(unpatch_custom_service_annotations) k8s.update_config(unpatch_custom_service_annotations)
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_statefulset_annotation_propagation(self):
'''
Inject annotation to Postgresql CRD and check it's propagation to stateful set
'''
k8s = self.k8s
cluster_label = 'application=spilo,cluster-name=acid-minimal-cluster'
patch_sset_propagate_annotations = {
"data": {
"downscaler_annotations": "deployment-time,downscaler/*",
}
}
k8s.update_config(patch_sset_propagate_annotations)
pg_crd_annotations = {
"metadata": {
"annotations": {
"deployment-time": "2020-04-30 12:00:00",
"downscaler/downtime_replicas": "0",
},
}
}
k8s.api.custom_objects_api.patch_namespaced_custom_object(
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_crd_annotations)
# wait a little before proceeding
time.sleep(60)
annotations = {
"deployment-time": "2020-04-30 12:00:00",
"downscaler/downtime_replicas": "0",
}
self.assertTrue(k8s.check_statefulset_annotations(cluster_label, annotations))
@timeout_decorator.timeout(TEST_TIMEOUT_SEC) @timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_taint_based_eviction(self): def test_taint_based_eviction(self):
''' '''
@ -528,7 +565,7 @@ class EndToEndTestCase(unittest.TestCase):
k8s.update_config(patch_toleration_config) k8s.update_config(patch_toleration_config)
# wait a little before proceeding with the pod distribution test # wait a little before proceeding with the pod distribution test
time.sleep(k8s.RETRY_TIMEOUT_SEC) time.sleep(30)
# toggle pod anti affinity to move replica away from master node # toggle pod anti affinity to move replica away from master node
self.assert_distributed_pods(new_master_node, new_replica_nodes, cluster_label) self.assert_distributed_pods(new_master_node, new_replica_nodes, cluster_label)
@ -694,10 +731,18 @@ class K8s:
def check_service_annotations(self, svc_labels, annotations, namespace='default'): def check_service_annotations(self, svc_labels, annotations, namespace='default'):
svcs = self.api.core_v1.list_namespaced_service(namespace, label_selector=svc_labels, limit=1).items svcs = self.api.core_v1.list_namespaced_service(namespace, label_selector=svc_labels, limit=1).items
for svc in svcs: for svc in svcs:
if len(svc.metadata.annotations) != len(annotations): for key, value in annotations.items():
return False if key not in svc.metadata.annotations or svc.metadata.annotations[key] != value:
for key in svc.metadata.annotations: print("Expected key {} not found in annotations {}".format(key, svc.metadata.annotation))
if svc.metadata.annotations[key] != annotations[key]: return False
return True
def check_statefulset_annotations(self, sset_labels, annotations, namespace='default'):
ssets = self.api.apps_v1.list_namespaced_stateful_set(namespace, label_selector=sset_labels, limit=1).items
for sset in ssets:
for key, value in annotations.items():
if key not in sset.metadata.annotations or sset.metadata.annotations[key] != value:
print("Expected key {} not found in annotations {}".format(key, sset.metadata.annotation))
return False return False
return True return True

View File

@ -30,6 +30,7 @@ data:
# default_memory_limit: 500Mi # default_memory_limit: 500Mi
# default_memory_request: 100Mi # default_memory_request: 100Mi
docker_image: registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p115 docker_image: registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p115
# downscaler_annotations: "deployment-time,downscaler/*"
# enable_admin_role_for_users: "true" # enable_admin_role_for_users: "true"
# enable_crd_validation: "true" # enable_crd_validation: "true"
# enable_database_access: "true" # enable_database_access: "true"
@ -49,16 +50,16 @@ data:
# inherited_labels: application,environment # inherited_labels: application,environment
# kube_iam_role: "" # kube_iam_role: ""
# log_s3_bucket: "" # log_s3_bucket: ""
# logical_backup_docker_image: "registry.opensource.zalan.do/acid/logical-backup" logical_backup_docker_image: "registry.opensource.zalan.do/acid/logical-backup"
# logical_backup_s3_access_key_id: "" # logical_backup_s3_access_key_id: ""
# logical_backup_s3_bucket: "my-bucket-url" logical_backup_s3_bucket: "my-bucket-url"
# logical_backup_s3_region: "" # logical_backup_s3_region: ""
# logical_backup_s3_endpoint: "" # logical_backup_s3_endpoint: ""
# logical_backup_s3_secret_access_key: "" # logical_backup_s3_secret_access_key: ""
# logical_backup_s3_sse: "AES256" logical_backup_s3_sse: "AES256"
# logical_backup_schedule: "30 00 * * *" logical_backup_schedule: "30 00 * * *"
master_dns_name_format: "{cluster}.{team}.{hostedzone}" master_dns_name_format: "{cluster}.{team}.{hostedzone}"
# master_pod_move_timeout: 10m # master_pod_move_timeout: 20m
# max_instances: "-1" # max_instances: "-1"
# min_instances: "-1" # min_instances: "-1"
# min_cpu_limit: 250m # min_cpu_limit: 250m

View File

@ -93,6 +93,10 @@ spec:
type: object type: object
additionalProperties: additionalProperties:
type: string type: string
downscaler_annotations:
type: array
items:
type: string
enable_init_containers: enable_init_containers:
type: boolean type: boolean
enable_pod_antiaffinity: enable_pod_antiaffinity:

View File

@ -31,6 +31,9 @@ configuration:
# custom_pod_annotations: # custom_pod_annotations:
# keya: valuea # keya: valuea
# keyb: valueb # keyb: valueb
# downscaler_annotations:
# - deployment-time
# - downscaler/*
enable_init_containers: true enable_init_containers: true
enable_pod_antiaffinity: false enable_pod_antiaffinity: false
enable_pod_disruption_budget: true enable_pod_disruption_budget: true

View File

@ -21,48 +21,48 @@ const (
// PostgresCRDResourceColumns definition of AdditionalPrinterColumns for postgresql CRD // PostgresCRDResourceColumns definition of AdditionalPrinterColumns for postgresql CRD
var PostgresCRDResourceColumns = []apiextv1beta1.CustomResourceColumnDefinition{ var PostgresCRDResourceColumns = []apiextv1beta1.CustomResourceColumnDefinition{
apiextv1beta1.CustomResourceColumnDefinition{ {
Name: "Team", Name: "Team",
Type: "string", Type: "string",
Description: "Team responsible for Postgres cluster", Description: "Team responsible for Postgres cluster",
JSONPath: ".spec.teamId", JSONPath: ".spec.teamId",
}, },
apiextv1beta1.CustomResourceColumnDefinition{ {
Name: "Version", Name: "Version",
Type: "string", Type: "string",
Description: "PostgreSQL version", Description: "PostgreSQL version",
JSONPath: ".spec.postgresql.version", JSONPath: ".spec.postgresql.version",
}, },
apiextv1beta1.CustomResourceColumnDefinition{ {
Name: "Pods", Name: "Pods",
Type: "integer", Type: "integer",
Description: "Number of Pods per Postgres cluster", Description: "Number of Pods per Postgres cluster",
JSONPath: ".spec.numberOfInstances", JSONPath: ".spec.numberOfInstances",
}, },
apiextv1beta1.CustomResourceColumnDefinition{ {
Name: "Volume", Name: "Volume",
Type: "string", Type: "string",
Description: "Size of the bound volume", Description: "Size of the bound volume",
JSONPath: ".spec.volume.size", JSONPath: ".spec.volume.size",
}, },
apiextv1beta1.CustomResourceColumnDefinition{ {
Name: "CPU-Request", Name: "CPU-Request",
Type: "string", Type: "string",
Description: "Requested CPU for Postgres containers", Description: "Requested CPU for Postgres containers",
JSONPath: ".spec.resources.requests.cpu", JSONPath: ".spec.resources.requests.cpu",
}, },
apiextv1beta1.CustomResourceColumnDefinition{ {
Name: "Memory-Request", Name: "Memory-Request",
Type: "string", Type: "string",
Description: "Requested memory for Postgres containers", Description: "Requested memory for Postgres containers",
JSONPath: ".spec.resources.requests.memory", JSONPath: ".spec.resources.requests.memory",
}, },
apiextv1beta1.CustomResourceColumnDefinition{ {
Name: "Age", Name: "Age",
Type: "date", Type: "date",
JSONPath: ".metadata.creationTimestamp", JSONPath: ".metadata.creationTimestamp",
}, },
apiextv1beta1.CustomResourceColumnDefinition{ {
Name: "Status", Name: "Status",
Type: "string", Type: "string",
Description: "Current sync status of postgresql resource", Description: "Current sync status of postgresql resource",
@ -72,31 +72,31 @@ var PostgresCRDResourceColumns = []apiextv1beta1.CustomResourceColumnDefinition{
// OperatorConfigCRDResourceColumns definition of AdditionalPrinterColumns for OperatorConfiguration CRD // OperatorConfigCRDResourceColumns definition of AdditionalPrinterColumns for OperatorConfiguration CRD
var OperatorConfigCRDResourceColumns = []apiextv1beta1.CustomResourceColumnDefinition{ var OperatorConfigCRDResourceColumns = []apiextv1beta1.CustomResourceColumnDefinition{
apiextv1beta1.CustomResourceColumnDefinition{ {
Name: "Image", Name: "Image",
Type: "string", Type: "string",
Description: "Spilo image to be used for Pods", Description: "Spilo image to be used for Pods",
JSONPath: ".configuration.docker_image", JSONPath: ".configuration.docker_image",
}, },
apiextv1beta1.CustomResourceColumnDefinition{ {
Name: "Cluster-Label", Name: "Cluster-Label",
Type: "string", Type: "string",
Description: "Label for K8s resources created by operator", Description: "Label for K8s resources created by operator",
JSONPath: ".configuration.kubernetes.cluster_name_label", JSONPath: ".configuration.kubernetes.cluster_name_label",
}, },
apiextv1beta1.CustomResourceColumnDefinition{ {
Name: "Service-Account", Name: "Service-Account",
Type: "string", Type: "string",
Description: "Name of service account to be used", Description: "Name of service account to be used",
JSONPath: ".configuration.kubernetes.pod_service_account_name", JSONPath: ".configuration.kubernetes.pod_service_account_name",
}, },
apiextv1beta1.CustomResourceColumnDefinition{ {
Name: "Min-Instances", Name: "Min-Instances",
Type: "integer", Type: "integer",
Description: "Minimum number of instances per Postgres cluster", Description: "Minimum number of instances per Postgres cluster",
JSONPath: ".configuration.min_instances", JSONPath: ".configuration.min_instances",
}, },
apiextv1beta1.CustomResourceColumnDefinition{ {
Name: "Age", Name: "Age",
Type: "date", Type: "date",
JSONPath: ".metadata.creationTimestamp", JSONPath: ".metadata.creationTimestamp",
@ -888,6 +888,14 @@ var OperatorConfigCRDResourceValidation = apiextv1beta1.CustomResourceValidation
}, },
}, },
}, },
"downscaler_annotations": {
Type: "array",
Items: &apiextv1beta1.JSONSchemaPropsOrArray{
Schema: &apiextv1beta1.JSONSchemaProps{
Type: "string",
},
},
},
"enable_init_containers": { "enable_init_containers": {
Type: "boolean", Type: "boolean",
}, },

View File

@ -62,6 +62,7 @@ type KubernetesMetaConfiguration struct {
PodRoleLabel string `json:"pod_role_label,omitempty"` PodRoleLabel string `json:"pod_role_label,omitempty"`
ClusterLabels map[string]string `json:"cluster_labels,omitempty"` ClusterLabels map[string]string `json:"cluster_labels,omitempty"`
InheritedLabels []string `json:"inherited_labels,omitempty"` InheritedLabels []string `json:"inherited_labels,omitempty"`
DownscalerAnnotations []string `json:"downscaler_annotations,omitempty"`
ClusterNameLabel string `json:"cluster_name_label,omitempty"` ClusterNameLabel string `json:"cluster_name_label,omitempty"`
NodeReadinessLabel map[string]string `json:"node_readiness_label,omitempty"` NodeReadinessLabel map[string]string `json:"node_readiness_label,omitempty"`
CustomPodAnnotations map[string]string `json:"custom_pod_annotations,omitempty"` CustomPodAnnotations map[string]string `json:"custom_pod_annotations,omitempty"`

View File

@ -180,6 +180,11 @@ func (in *KubernetesMetaConfiguration) DeepCopyInto(out *KubernetesMetaConfigura
*out = make([]string, len(*in)) *out = make([]string, len(*in))
copy(*out, *in) copy(*out, *in)
} }
if in.DownscalerAnnotations != nil {
in, out := &in.DownscalerAnnotations, &out.DownscalerAnnotations
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.NodeReadinessLabel != nil { if in.NodeReadinessLabel != nil {
in, out := &in.NodeReadinessLabel, &out.NodeReadinessLabel in, out := &in.NodeReadinessLabel, &out.NodeReadinessLabel
*out = make(map[string]string, len(*in)) *out = make(map[string]string, len(*in))

View File

@ -711,8 +711,7 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
updateFailed = true updateFailed = true
return return
} }
if !reflect.DeepEqual(oldSs, newSs) || !reflect.DeepEqual(oldSpec.Annotations, newSpec.Annotations) {
if !reflect.DeepEqual(oldSs, newSs) {
c.logger.Debugf("syncing statefulsets") c.logger.Debugf("syncing statefulsets")
// TODO: avoid generating the StatefulSet object twice by passing it to syncStatefulSet // TODO: avoid generating the StatefulSet object twice by passing it to syncStatefulSet
if err := c.syncStatefulSet(); err != nil { if err := c.syncStatefulSet(); err != nil {

View File

@ -28,19 +28,48 @@ var eventRecorder = record.NewFakeRecorder(1)
var cl = New( var cl = New(
Config{ Config{
OpConfig: config.Config{ OpConfig: config.Config{
ProtectedRoles: []string{"admin"}, PodManagementPolicy: "ordered_ready",
ProtectedRoles: []string{"admin"},
Auth: config.Auth{ Auth: config.Auth{
SuperUsername: superUserName, SuperUsername: superUserName,
ReplicationUsername: replicationUserName, ReplicationUsername: replicationUserName,
}, },
Resources: config.Resources{
DownscalerAnnotations: []string{"downscaler/*"},
},
}, },
}, },
k8sutil.NewMockKubernetesClient(), k8sutil.NewMockKubernetesClient(),
acidv1.Postgresql{ObjectMeta: metav1.ObjectMeta{Name: "acid-test", Namespace: "test"}}, acidv1.Postgresql{ObjectMeta: metav1.ObjectMeta{Name: "acid-test", Namespace: "test", Annotations: map[string]string{"downscaler/downtime_replicas": "0"}}},
logger, logger,
eventRecorder, eventRecorder,
) )
func TestStatefulSetAnnotations(t *testing.T) {
testName := "CheckStatefulsetAnnotations"
spec := acidv1.PostgresSpec{
TeamID: "myapp", NumberOfInstances: 1,
Resources: acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
},
Volume: acidv1.Volume{
Size: "1G",
},
}
ss, err := cl.generateStatefulSet(&spec)
if err != nil {
t.Errorf("in %s no statefulset created %v", testName, err)
}
if ss != nil {
annotation := ss.ObjectMeta.GetAnnotations()
if _, ok := annotation["downscaler/downtime_replicas"]; !ok {
t.Errorf("in %s respective annotation not found on sts", testName)
}
}
}
func TestInitRobotUsers(t *testing.T) { func TestInitRobotUsers(t *testing.T) {
testName := "TestInitRobotUsers" testName := "TestInitRobotUsers"
tests := []struct { tests := []struct {

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"path" "path"
"sort" "sort"
"strconv"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -1182,12 +1183,15 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
return nil, fmt.Errorf("could not set the pod management policy to the unknown value: %v", c.OpConfig.PodManagementPolicy) return nil, fmt.Errorf("could not set the pod management policy to the unknown value: %v", c.OpConfig.PodManagementPolicy)
} }
annotations = make(map[string]string)
annotations[rollingUpdateStatefulsetAnnotationKey] = strconv.FormatBool(false)
statefulSet := &appsv1.StatefulSet{ statefulSet := &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: c.statefulSetName(), Name: c.statefulSetName(),
Namespace: c.Namespace, Namespace: c.Namespace,
Labels: c.labelsSet(true), Labels: c.labelsSet(true),
Annotations: map[string]string{rollingUpdateStatefulsetAnnotationKey: "false"}, Annotations: c.AnnotationsToPropagate(annotations),
}, },
Spec: appsv1.StatefulSetSpec{ Spec: appsv1.StatefulSetSpec{
Replicas: &numberOfInstances, Replicas: &numberOfInstances,

View File

@ -853,3 +853,24 @@ func (c *Cluster) updateConnectionPoolerDeployment(oldDeploymentSpec, newDeploym
return deployment, nil return deployment, nil
} }
//updateConnectionPoolerAnnotations updates the annotations of connection pooler deployment
func (c *Cluster) updateConnectionPoolerAnnotations(annotations map[string]string) (*appsv1.Deployment, error) {
c.logger.Debugf("updating connection pooler annotations")
patchData, err := metaAnnotationsPatch(annotations)
if err != nil {
return nil, fmt.Errorf("could not form patch for the deployment metadata: %v", err)
}
result, err := c.KubeClient.Deployments(c.ConnectionPooler.Deployment.Namespace).Patch(
context.TODO(),
c.ConnectionPooler.Deployment.Name,
types.MergePatchType,
[]byte(patchData),
metav1.PatchOptions{},
"")
if err != nil {
return nil, fmt.Errorf("could not patch connection pooler annotations %q: %v", patchData, err)
}
return result, nil
}

View File

@ -3,6 +3,7 @@ package cluster
import ( import (
"context" "context"
"fmt" "fmt"
"regexp"
"strings" "strings"
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
@ -358,6 +359,8 @@ func (c *Cluster) syncStatefulSet() error {
} }
} }
} }
annotations := c.AnnotationsToPropagate(c.Statefulset.Annotations)
c.updateStatefulSetAnnotations(annotations)
if !podsRollingUpdateRequired && !c.OpConfig.EnableLazySpiloUpgrade { if !podsRollingUpdateRequired && !c.OpConfig.EnableLazySpiloUpgrade {
// even if desired and actual statefulsets match // even if desired and actual statefulsets match
@ -397,6 +400,30 @@ func (c *Cluster) syncStatefulSet() error {
return nil return nil
} }
// AnnotationsToPropagate get the annotations to update if required
// based on the annotations in postgres CRD
func (c *Cluster) AnnotationsToPropagate(annotations map[string]string) map[string]string {
toPropagateAnnotations := c.OpConfig.DownscalerAnnotations
pgCRDAnnotations := c.Postgresql.ObjectMeta.GetAnnotations()
if toPropagateAnnotations != nil && pgCRDAnnotations != nil {
for _, anno := range toPropagateAnnotations {
for k, v := range pgCRDAnnotations {
matched, err := regexp.MatchString(anno, k)
if err != nil {
c.logger.Errorf("annotations matching issue: %v", err)
return nil
}
if matched {
annotations[k] = v
}
}
}
}
return annotations
}
// checkAndSetGlobalPostgreSQLConfiguration checks whether cluster-wide API parameters // checkAndSetGlobalPostgreSQLConfiguration checks whether cluster-wide API parameters
// (like max_connections) has changed and if necessary sets it via the Patroni API // (like max_connections) has changed and if necessary sets it via the Patroni API
func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration() error { func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration() error {
@ -939,6 +966,11 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql
} }
} }
newAnnotations := c.AnnotationsToPropagate(c.ConnectionPooler.Deployment.Annotations)
if newAnnotations != nil {
c.updateConnectionPoolerAnnotations(newAnnotations)
}
service, err := c.KubeClient. service, err := c.KubeClient.
Services(c.Namespace). Services(c.Namespace).
Get(context.TODO(), c.connectionPoolerName(), metav1.GetOptions{}) Get(context.TODO(), c.connectionPoolerName(), metav1.GetOptions{})

View File

@ -33,28 +33,28 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result := &config.Config{} result := &config.Config{}
// general config // general config
result.EnableCRDValidation = fromCRD.EnableCRDValidation result.EnableCRDValidation = util.CoalesceBool(fromCRD.EnableCRDValidation, util.True())
result.EnableLazySpiloUpgrade = fromCRD.EnableLazySpiloUpgrade result.EnableLazySpiloUpgrade = fromCRD.EnableLazySpiloUpgrade
result.EtcdHost = fromCRD.EtcdHost result.EtcdHost = fromCRD.EtcdHost
result.KubernetesUseConfigMaps = fromCRD.KubernetesUseConfigMaps result.KubernetesUseConfigMaps = fromCRD.KubernetesUseConfigMaps
result.DockerImage = fromCRD.DockerImage result.DockerImage = util.Coalesce(fromCRD.DockerImage, "registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p115")
result.Workers = fromCRD.Workers result.Workers = fromCRD.Workers
result.MinInstances = fromCRD.MinInstances result.MinInstances = fromCRD.MinInstances
result.MaxInstances = fromCRD.MaxInstances result.MaxInstances = fromCRD.MaxInstances
result.ResyncPeriod = time.Duration(fromCRD.ResyncPeriod) result.ResyncPeriod = time.Duration(fromCRD.ResyncPeriod)
result.RepairPeriod = time.Duration(fromCRD.RepairPeriod) result.RepairPeriod = time.Duration(fromCRD.RepairPeriod)
result.SetMemoryRequestToLimit = fromCRD.SetMemoryRequestToLimit result.SetMemoryRequestToLimit = fromCRD.SetMemoryRequestToLimit
result.ShmVolume = fromCRD.ShmVolume result.ShmVolume = util.CoalesceBool(fromCRD.ShmVolume, util.True())
result.SidecarImages = fromCRD.SidecarImages result.SidecarImages = fromCRD.SidecarImages
result.SidecarContainers = fromCRD.SidecarContainers result.SidecarContainers = fromCRD.SidecarContainers
// user config // user config
result.SuperUsername = fromCRD.PostgresUsersConfiguration.SuperUsername result.SuperUsername = util.Coalesce(fromCRD.PostgresUsersConfiguration.SuperUsername, "postgres")
result.ReplicationUsername = fromCRD.PostgresUsersConfiguration.ReplicationUsername result.ReplicationUsername = util.Coalesce(fromCRD.PostgresUsersConfiguration.ReplicationUsername, "standby")
// kubernetes config // kubernetes config
result.CustomPodAnnotations = fromCRD.Kubernetes.CustomPodAnnotations result.CustomPodAnnotations = fromCRD.Kubernetes.CustomPodAnnotations
result.PodServiceAccountName = fromCRD.Kubernetes.PodServiceAccountName result.PodServiceAccountName = util.Coalesce(fromCRD.Kubernetes.PodServiceAccountName, "postgres-pod")
result.PodServiceAccountDefinition = fromCRD.Kubernetes.PodServiceAccountDefinition result.PodServiceAccountDefinition = fromCRD.Kubernetes.PodServiceAccountDefinition
result.PodServiceAccountRoleBindingDefinition = fromCRD.Kubernetes.PodServiceAccountRoleBindingDefinition result.PodServiceAccountRoleBindingDefinition = fromCRD.Kubernetes.PodServiceAccountRoleBindingDefinition
result.PodEnvironmentConfigMap = fromCRD.Kubernetes.PodEnvironmentConfigMap result.PodEnvironmentConfigMap = fromCRD.Kubernetes.PodEnvironmentConfigMap
@ -64,30 +64,31 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.ClusterDomain = util.Coalesce(fromCRD.Kubernetes.ClusterDomain, "cluster.local") result.ClusterDomain = util.Coalesce(fromCRD.Kubernetes.ClusterDomain, "cluster.local")
result.WatchedNamespace = fromCRD.Kubernetes.WatchedNamespace result.WatchedNamespace = fromCRD.Kubernetes.WatchedNamespace
result.PDBNameFormat = fromCRD.Kubernetes.PDBNameFormat result.PDBNameFormat = fromCRD.Kubernetes.PDBNameFormat
result.EnablePodDisruptionBudget = fromCRD.Kubernetes.EnablePodDisruptionBudget result.EnablePodDisruptionBudget = util.CoalesceBool(fromCRD.Kubernetes.EnablePodDisruptionBudget, util.True())
result.EnableInitContainers = fromCRD.Kubernetes.EnableInitContainers result.EnableInitContainers = util.CoalesceBool(fromCRD.Kubernetes.EnableInitContainers, util.True())
result.EnableSidecars = fromCRD.Kubernetes.EnableSidecars result.EnableSidecars = util.CoalesceBool(fromCRD.Kubernetes.EnableSidecars, util.True())
result.SecretNameTemplate = fromCRD.Kubernetes.SecretNameTemplate result.SecretNameTemplate = fromCRD.Kubernetes.SecretNameTemplate
result.OAuthTokenSecretName = fromCRD.Kubernetes.OAuthTokenSecretName result.OAuthTokenSecretName = fromCRD.Kubernetes.OAuthTokenSecretName
result.InfrastructureRolesSecretName = fromCRD.Kubernetes.InfrastructureRolesSecretName result.InfrastructureRolesSecretName = fromCRD.Kubernetes.InfrastructureRolesSecretName
result.PodRoleLabel = fromCRD.Kubernetes.PodRoleLabel result.PodRoleLabel = util.Coalesce(fromCRD.Kubernetes.PodRoleLabel, "spilo-role")
result.ClusterLabels = fromCRD.Kubernetes.ClusterLabels result.ClusterLabels = fromCRD.Kubernetes.ClusterLabels
result.InheritedLabels = fromCRD.Kubernetes.InheritedLabels result.InheritedLabels = fromCRD.Kubernetes.InheritedLabels
result.ClusterNameLabel = fromCRD.Kubernetes.ClusterNameLabel result.DownscalerAnnotations = fromCRD.Kubernetes.DownscalerAnnotations
result.ClusterNameLabel = util.Coalesce(fromCRD.Kubernetes.ClusterNameLabel, "cluster-name")
result.NodeReadinessLabel = fromCRD.Kubernetes.NodeReadinessLabel result.NodeReadinessLabel = fromCRD.Kubernetes.NodeReadinessLabel
result.PodPriorityClassName = fromCRD.Kubernetes.PodPriorityClassName result.PodPriorityClassName = fromCRD.Kubernetes.PodPriorityClassName
result.PodManagementPolicy = fromCRD.Kubernetes.PodManagementPolicy result.PodManagementPolicy = util.Coalesce(fromCRD.Kubernetes.PodManagementPolicy, "ordered_ready")
result.MasterPodMoveTimeout = time.Duration(fromCRD.Kubernetes.MasterPodMoveTimeout) result.MasterPodMoveTimeout = time.Duration(fromCRD.Kubernetes.MasterPodMoveTimeout)
result.EnablePodAntiAffinity = fromCRD.Kubernetes.EnablePodAntiAffinity result.EnablePodAntiAffinity = fromCRD.Kubernetes.EnablePodAntiAffinity
result.PodAntiAffinityTopologyKey = fromCRD.Kubernetes.PodAntiAffinityTopologyKey result.PodAntiAffinityTopologyKey = util.Coalesce(fromCRD.Kubernetes.PodAntiAffinityTopologyKey, "kubernetes.io/hostname")
// Postgres Pod resources // Postgres Pod resources
result.DefaultCPURequest = fromCRD.PostgresPodResources.DefaultCPURequest result.DefaultCPURequest = util.Coalesce(fromCRD.PostgresPodResources.DefaultCPURequest, "100m")
result.DefaultMemoryRequest = fromCRD.PostgresPodResources.DefaultMemoryRequest result.DefaultMemoryRequest = util.Coalesce(fromCRD.PostgresPodResources.DefaultMemoryRequest, "100Mi")
result.DefaultCPULimit = fromCRD.PostgresPodResources.DefaultCPULimit result.DefaultCPULimit = util.Coalesce(fromCRD.PostgresPodResources.DefaultCPULimit, "1")
result.DefaultMemoryLimit = fromCRD.PostgresPodResources.DefaultMemoryLimit result.DefaultMemoryLimit = util.Coalesce(fromCRD.PostgresPodResources.DefaultMemoryLimit, "500Mi")
result.MinCPULimit = fromCRD.PostgresPodResources.MinCPULimit result.MinCPULimit = util.Coalesce(fromCRD.PostgresPodResources.MinCPULimit, "250m")
result.MinMemoryLimit = fromCRD.PostgresPodResources.MinMemoryLimit result.MinMemoryLimit = util.Coalesce(fromCRD.PostgresPodResources.MinMemoryLimit, "250Mi")
// timeout config // timeout config
result.ResourceCheckInterval = time.Duration(fromCRD.Timeouts.ResourceCheckInterval) result.ResourceCheckInterval = time.Duration(fromCRD.Timeouts.ResourceCheckInterval)
@ -114,8 +115,8 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.AdditionalSecretMountPath = fromCRD.AWSGCP.AdditionalSecretMountPath result.AdditionalSecretMountPath = fromCRD.AWSGCP.AdditionalSecretMountPath
// logical backup config // logical backup config
result.LogicalBackupSchedule = fromCRD.LogicalBackup.Schedule result.LogicalBackupSchedule = util.Coalesce(fromCRD.LogicalBackup.Schedule, "30 00 * * *")
result.LogicalBackupDockerImage = fromCRD.LogicalBackup.DockerImage result.LogicalBackupDockerImage = util.Coalesce(fromCRD.LogicalBackup.DockerImage, "registry.opensource.zalan.do/acid/logical-backup")
result.LogicalBackupS3Bucket = fromCRD.LogicalBackup.S3Bucket result.LogicalBackupS3Bucket = fromCRD.LogicalBackup.S3Bucket
result.LogicalBackupS3Region = fromCRD.LogicalBackup.S3Region result.LogicalBackupS3Region = fromCRD.LogicalBackup.S3Region
result.LogicalBackupS3Endpoint = fromCRD.LogicalBackup.S3Endpoint result.LogicalBackupS3Endpoint = fromCRD.LogicalBackup.S3Endpoint

View File

@ -487,7 +487,9 @@ func (c *Controller) postgresqlUpdate(prev, cur interface{}) {
if pgOld != nil && pgNew != nil { if pgOld != nil && pgNew != nil {
// Avoid the inifinite recursion for status updates // Avoid the inifinite recursion for status updates
if reflect.DeepEqual(pgOld.Spec, pgNew.Spec) { if reflect.DeepEqual(pgOld.Spec, pgNew.Spec) {
return if reflect.DeepEqual(pgNew.Annotations, pgOld.Annotations) {
return
}
} }
c.queueClusterEvent(pgOld, pgNew, EventUpdate) c.queueClusterEvent(pgOld, pgNew, EventUpdate)
} }

View File

@ -34,6 +34,7 @@ type Resources struct {
SpiloPrivileged bool `name:"spilo_privileged" default:"false"` SpiloPrivileged bool `name:"spilo_privileged" default:"false"`
ClusterLabels map[string]string `name:"cluster_labels" default:"application:spilo"` ClusterLabels map[string]string `name:"cluster_labels" default:"application:spilo"`
InheritedLabels []string `name:"inherited_labels" default:""` InheritedLabels []string `name:"inherited_labels" default:""`
DownscalerAnnotations []string `name:"downscaler_annotations"`
ClusterNameLabel string `name:"cluster_name_label" default:"cluster-name"` ClusterNameLabel string `name:"cluster_name_label" default:"cluster-name"`
PodRoleLabel string `name:"pod_role_label" default:"spilo-role"` PodRoleLabel string `name:"pod_role_label" default:"spilo-role"`
PodToleration map[string]string `name:"toleration" default:""` PodToleration map[string]string `name:"toleration" default:""`
@ -82,7 +83,7 @@ type LogicalBackup struct {
LogicalBackupS3Endpoint string `name:"logical_backup_s3_endpoint" default:""` LogicalBackupS3Endpoint string `name:"logical_backup_s3_endpoint" default:""`
LogicalBackupS3AccessKeyID string `name:"logical_backup_s3_access_key_id" default:""` LogicalBackupS3AccessKeyID string `name:"logical_backup_s3_access_key_id" default:""`
LogicalBackupS3SecretAccessKey string `name:"logical_backup_s3_secret_access_key" default:""` LogicalBackupS3SecretAccessKey string `name:"logical_backup_s3_secret_access_key" default:""`
LogicalBackupS3SSE string `name:"logical_backup_s3_sse" default:"AES256"` LogicalBackupS3SSE string `name:"logical_backup_s3_sse" default:""`
} }
// Operator options for connection pooler // Operator options for connection pooler

View File

@ -147,7 +147,7 @@ func Coalesce(val, defaultVal string) string {
return val return val
} }
// Yeah, golang // CoalesceInt32 works like coalesce but for *int32
func CoalesceInt32(val, defaultVal *int32) *int32 { func CoalesceInt32(val, defaultVal *int32) *int32 {
if val == nil { if val == nil {
return defaultVal return defaultVal
@ -155,6 +155,14 @@ func CoalesceInt32(val, defaultVal *int32) *int32 {
return val return val
} }
// CoalesceBool works like coalesce but for *bool
func CoalesceBool(val, defaultVal *bool) *bool {
if val == nil {
return defaultVal
}
return val
}
// Test if any of the values is nil // Test if any of the values is nil
func testNil(values ...*int32) bool { func testNil(values ...*int32) bool {
for _, v := range values { for _, v := range values {
@ -166,8 +174,8 @@ func testNil(values ...*int32) bool {
return false return false
} }
// Return maximum of two integers provided via pointers. If one value is not // MaxInt32 : Return maximum of two integers provided via pointers. If one value
// defined, return the other one. If both are not defined, result is also // is not defined, return the other one. If both are not defined, result is also
// undefined, caller needs to check for that. // undefined, caller needs to check for that.
func MaxInt32(a, b *int32) *int32 { func MaxInt32(a, b *int32) *int32 {
if testNil(a, b) { if testNil(a, b) {