Merge branch 'master' into stand

This commit is contained in:
Felix Kunde
2022-03-30 14:49:42 +02:00
committed by GitHub
50 changed files with 2315 additions and 918 deletions
@@ -23,6 +23,9 @@ metadata:
{{- toYaml . | nindent 4 }} {{- toYaml . | nindent 4 }}
{{- end }} {{- end }}
spec: spec:
{{- if .Values.ingress.ingressClassName }}
ingressClassName: {{ .Values.ingress.ingressClassName }}
{{- end }}
{{- if .Values.ingress.tls }} {{- if .Values.ingress.tls }}
tls: tls:
{{- range .Values.ingress.tls }} {{- range .Values.ingress.tls }}
+1
View File
@@ -93,6 +93,7 @@ ingress:
{} {}
# kubernetes.io/ingress.class: nginx # kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true" # kubernetes.io/tls-acme: "true"
ingressClassName: ""
hosts: hosts:
- host: ui.example.org - host: ui.example.org
paths: [""] paths: [""]
@@ -130,6 +130,11 @@ spec:
users: users:
type: object type: object
properties: properties:
additional_owner_roles:
type: array
nullable: true
items:
type: string
enable_password_rotation: enable_password_rotation:
type: boolean type: boolean
default: false default: false
@@ -207,6 +212,10 @@ spec:
enable_sidecars: enable_sidecars:
type: boolean type: boolean
default: true default: true
ignored_annotations:
type: array
items:
type: string
infrastructure_roles_secret_name: infrastructure_roles_secret_name:
type: string type: string
infrastructure_roles_secrets: infrastructure_roles_secrets:
@@ -349,6 +358,12 @@ spec:
timeouts: timeouts:
type: object type: object
properties: properties:
patroni_api_check_interval:
type: string
default: "1s"
patroni_api_check_timeout:
type: string
default: "5s"
pod_label_wait_timeout: pod_label_wait_timeout:
type: string type: string
default: "10m" default: "10m"
@@ -380,9 +395,15 @@ spec:
enable_master_load_balancer: enable_master_load_balancer:
type: boolean type: boolean
default: true default: true
enable_master_pooler_load_balancer:
type: boolean
default: false
enable_replica_load_balancer: enable_replica_load_balancer:
type: boolean type: boolean
default: false default: false
enable_replica_pooler_load_balancer:
type: boolean
default: false
external_traffic_policy: external_traffic_policy:
type: string type: string
enum: enum:
@@ -450,6 +471,8 @@ spec:
type: string type: string
logical_backup_s3_sse: logical_backup_s3_sse:
type: string type: string
logical_backup_s3_retention_time:
type: string
logical_backup_schedule: logical_backup_schedule:
type: string type: string
pattern: '^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$' pattern: '^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$'
@@ -500,6 +523,7 @@ spec:
type: string type: string
default: default:
- admin - admin
- cron_admin
role_deletion_suffix: role_deletion_suffix:
type: string type: string
default: "_deleted" default: "_deleted"
+4 -22
View File
@@ -150,15 +150,9 @@ spec:
minimum: 1 minimum: 1
resources: resources:
type: object type: object
required:
- requests
- limits
properties: properties:
limits: limits:
type: object type: object
required:
- cpu
- memory
properties: properties:
cpu: cpu:
type: string type: string
@@ -168,9 +162,6 @@ spec:
pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$'
requests: requests:
type: object type: object
required:
- cpu
- memory
properties: properties:
cpu: cpu:
type: string type: string
@@ -197,8 +188,12 @@ spec:
type: boolean type: boolean
enableMasterLoadBalancer: enableMasterLoadBalancer:
type: boolean type: boolean
enableMasterPoolerLoadBalancer:
type: boolean
enableReplicaLoadBalancer: enableReplicaLoadBalancer:
type: boolean type: boolean
enableReplicaPoolerLoadBalancer:
type: boolean
enableShmVolume: enableShmVolume:
type: boolean type: boolean
init_containers: init_containers:
@@ -402,15 +397,9 @@ spec:
description: deprecated description: deprecated
resources: resources:
type: object type: object
required:
- requests
- limits
properties: properties:
limits: limits:
type: object type: object
required:
- cpu
- memory
properties: properties:
cpu: cpu:
type: string type: string
@@ -439,9 +428,6 @@ spec:
# than the corresponding limit. # than the corresponding limit.
requests: requests:
type: object type: object
required:
- cpu
- memory
properties: properties:
cpu: cpu:
type: string type: string
@@ -532,10 +518,6 @@ spec:
type: array type: array
items: items:
type: object type: object
required:
- key
- operator
- effect
properties: properties:
key: key:
type: string type: string
+26
View File
@@ -59,6 +59,16 @@ configGeneral:
# parameters describing Postgres users # parameters describing Postgres users
configUsers: configUsers:
# roles to be granted to database owners
# additional_owner_roles:
# - cron_admin
# enable password rotation for app users that are not database owners
enable_password_rotation: false
# rotation interval for updating credentials in K8s secrets of app users
password_rotation_interval: 90
# retention interval to keep rotation users
password_rotation_user_retention: 180
# postgres username used for replication between instances # postgres username used for replication between instances
replication_username: standby replication_username: standby
# postgres superuser name to be created by initdb # postgres superuser name to be created by initdb
@@ -114,6 +124,11 @@ configKubernetes:
enable_pod_disruption_budget: true enable_pod_disruption_budget: true
# enables sidecar containers to run alongside Spilo in the same pod # enables sidecar containers to run alongside Spilo in the same pod
enable_sidecars: true enable_sidecars: true
# annotations to be ignored when comparing statefulsets, services etc.
# ignored_annotations:
# - k8s.v1.cni.cncf.io/network-status
# namespaced name of the secret containing infrastructure roles names and passwords # namespaced name of the secret containing infrastructure roles names and passwords
# infrastructure_roles_secret_name: postgresql-infrastructure-roles # infrastructure_roles_secret_name: postgresql-infrastructure-roles
@@ -204,6 +219,10 @@ configPostgresPodResources:
# timeouts related to some operator actions # timeouts related to some operator actions
configTimeouts: configTimeouts:
# interval between consecutive attempts of operator calling the Patroni API
patroni_api_check_interval: 1s
# timeout when waiting for successful response from Patroni API
patroni_api_check_timeout: 5s
# timeout when waiting for the Postgres pods to be deleted # timeout when waiting for the Postgres pods to be deleted
pod_deletion_wait_timeout: 10m pod_deletion_wait_timeout: 10m
# timeout when waiting for pod role and cluster labels # timeout when waiting for pod role and cluster labels
@@ -228,8 +247,12 @@ configLoadBalancer:
# toggles service type load balancer pointing to the master pod of the cluster # toggles service type load balancer pointing to the master pod of the cluster
enable_master_load_balancer: false enable_master_load_balancer: false
# toggles service type load balancer pointing to the master pooler pod of the cluster
enable_master_pooler_load_balancer: false
# toggles service type load balancer pointing to the replica pod of the cluster # toggles service type load balancer pointing to the replica pod of the cluster
enable_replica_load_balancer: false enable_replica_load_balancer: false
# toggles service type load balancer pointing to the replica pooler pod of the cluster
enable_replica_pooler_load_balancer: false
# define external traffic policy for the load balancer # define external traffic policy for the load balancer
external_traffic_policy: "Cluster" external_traffic_policy: "Cluster"
# defines the DNS name string template for the master load balancer cluster # defines the DNS name string template for the master load balancer cluster
@@ -310,6 +333,8 @@ configLogicalBackup:
logical_backup_s3_secret_access_key: "" logical_backup_s3_secret_access_key: ""
# S3 server side encryption # S3 server side encryption
logical_backup_s3_sse: "AES256" logical_backup_s3_sse: "AES256"
# S3 retention time for stored backups for example "2 week" or "7 days"
logical_backup_s3_retention_time: ""
# backup schedule in the cron format # backup schedule in the cron format
logical_backup_schedule: "30 00 * * *" logical_backup_schedule: "30 00 * * *"
@@ -338,6 +363,7 @@ configTeamsApi:
# List of roles that cannot be overwritten by an application, team or infrastructure role # List of roles that cannot be overwritten by an application, team or infrastructure role
protected_role_names: protected_role_names:
- admin - admin
- cron_admin
# Suffix to add if members are removed from TeamsAPI or PostgresTeam CRD # Suffix to add if members are removed from TeamsAPI or PostgresTeam CRD
role_deletion_suffix: "_deleted" role_deletion_suffix: "_deleted"
# role name to grant to team members created from the Teams API # role name to grant to team members created from the Teams API
+2 -1
View File
@@ -2,13 +2,14 @@ package main
import ( import (
"flag" "flag"
log "github.com/sirupsen/logrus"
"os" "os"
"os/signal" "os/signal"
"sync" "sync"
"syscall" "syscall"
"time" "time"
log "github.com/sirupsen/logrus"
"github.com/zalando/postgres-operator/pkg/controller" "github.com/zalando/postgres-operator/pkg/controller"
"github.com/zalando/postgres-operator/pkg/spec" "github.com/zalando/postgres-operator/pkg/spec"
"github.com/zalando/postgres-operator/pkg/util/k8sutil" "github.com/zalando/postgres-operator/pkg/util/k8sutil"
+65 -1
View File
@@ -12,9 +12,21 @@ DUMP_SIZE_COEFF=5
ERRORCOUNT=0 ERRORCOUNT=0
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
K8S_API_URL=https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT/api/v1 if [ "$KUBERNETES_SERVICE_HOST" != "${KUBERNETES_SERVICE_HOST#*[0-9].[0-9]}" ]; then
echo "IPv4"
K8S_API_URL=https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT/api/v1
elif [ "$KUBERNETES_SERVICE_HOST" != "${KUBERNETES_SERVICE_HOST#*:[0-9a-fA-F]}" ]; then
echo "IPv6"
K8S_API_URL=https://[$KUBERNETES_SERVICE_HOST]:$KUBERNETES_SERVICE_PORT/api/v1
else
echo "Unrecognized IP format '$KUBERNETES_SERVICE_HOST'"
fi
echo "API Endpoint: ${K8S_API_URL}"
CERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt CERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
LOGICAL_BACKUP_PROVIDER=${LOGICAL_BACKUP_PROVIDER:="s3"}
LOGICAL_BACKUP_S3_RETENTION_TIME=${LOGICAL_BACKUP_S3_RETENTION_TIME:=""}
function estimate_size { function estimate_size {
"$PG_BIN"/psql -tqAc "${ALL_DB_SIZE_QUERY}" "$PG_BIN"/psql -tqAc "${ALL_DB_SIZE_QUERY}"
} }
@@ -28,6 +40,57 @@ function compress {
pigz pigz
} }
function aws_delete_objects {
args=(
"--bucket=$LOGICAL_BACKUP_S3_BUCKET"
)
[[ ! -z "$LOGICAL_BACKUP_S3_ENDPOINT" ]] && args+=("--endpoint-url=$LOGICAL_BACKUP_S3_ENDPOINT")
[[ ! -z "$LOGICAL_BACKUP_S3_REGION" ]] && args+=("--region=$LOGICAL_BACKUP_S3_REGION")
aws s3api delete-objects "${args[@]}" --delete Objects=["$(printf {Key=%q}, "$@")"],Quiet=true
}
export -f aws_delete_objects
function aws_delete_outdated {
if [[ -z "$LOGICAL_BACKUP_S3_RETENTION_TIME" ]] ; then
echo "no retention time configured: skip cleanup of outdated backups"
return 0
fi
# define cutoff date for outdated backups (day precision)
cutoff_date=$(date -d "$LOGICAL_BACKUP_S3_RETENTION_TIME ago" +%F)
# mimic bucket setup from Spilo
prefix="spilo/"$SCOPE$LOGICAL_BACKUP_S3_BUCKET_SCOPE_SUFFIX"/logical_backups/"
args=(
"--no-paginate"
"--output=text"
"--prefix=$prefix"
"--bucket=$LOGICAL_BACKUP_S3_BUCKET"
)
[[ ! -z "$LOGICAL_BACKUP_S3_ENDPOINT" ]] && args+=("--endpoint-url=$LOGICAL_BACKUP_S3_ENDPOINT")
[[ ! -z "$LOGICAL_BACKUP_S3_REGION" ]] && args+=("--region=$LOGICAL_BACKUP_S3_REGION")
# list objects older than the cutoff date
aws s3api list-objects "${args[@]}" --query="Contents[?LastModified<='$cutoff_date'].[Key]" > /tmp/outdated-backups
# spare the last backup
sed -i '$d' /tmp/outdated-backups
count=$(wc -l < /tmp/outdated-backups)
if [[ $count == 0 ]] ; then
echo "no outdated backups to delete"
return 0
fi
echo "deleting $count outdated backups created before $cutoff_date"
# deleted outdated files in batches with 100 at a time
tr '\n' '\0' < /tmp/outdated-backups | xargs -0 -P1 -n100 bash -c 'aws_delete_objects "$@"' _
}
function aws_upload { function aws_upload {
declare -r EXPECTED_SIZE="$1" declare -r EXPECTED_SIZE="$1"
@@ -59,6 +122,7 @@ function upload {
;; ;;
*) *)
aws_upload $(($(estimate_size) / DUMP_SIZE_COEFF)) aws_upload $(($(estimate_size) / DUMP_SIZE_COEFF))
aws_delete_outdated
;; ;;
esac esac
} }
+5
View File
@@ -750,6 +750,11 @@ lead to K8s removing this field from the manifest due to its
Then the resultant manifest will not contain the necessary change, and the Then the resultant manifest will not contain the necessary change, and the
operator will respectively do nothing with the existing source ranges. operator will respectively do nothing with the existing source ranges.
Load balancer services can also be enabled for the [connection pooler](user.md#connection-pooler)
pods with manifest flags `enableMasterPoolerLoadBalancer` and/or
`enableReplicaPoolerLoadBalancer` or in the operator configuration with
`enable_master_pooler_load_balancer` and/or `enable_replica_pooler_load_balancer`.
## Running periodic 'autorepair' scans of K8s objects ## Running periodic 'autorepair' scans of K8s objects
The Postgres Operator periodically scans all K8s objects belonging to each The Postgres Operator periodically scans all K8s objects belonging to each
+13 -3
View File
@@ -91,11 +91,23 @@ These parameters are grouped directly under the `spec` key in the manifest.
`enable_master_load_balancer` parameter) to define whether to enable the load `enable_master_load_balancer` parameter) to define whether to enable the load
balancer pointing to the Postgres primary. Optional. balancer pointing to the Postgres primary. Optional.
* **enableMasterPoolerLoadBalancer**
boolean flag to override the operator defaults (set by the
`enable_master_pooler_load_balancer` parameter) to define whether to enable
the load balancer for master pooler pods pointing to the Postgres primary.
Optional.
* **enableReplicaLoadBalancer** * **enableReplicaLoadBalancer**
boolean flag to override the operator defaults (set by the boolean flag to override the operator defaults (set by the
`enable_replica_load_balancer` parameter) to define whether to enable the `enable_replica_load_balancer` parameter) to define whether to enable the
load balancer pointing to the Postgres standby instances. Optional. load balancer pointing to the Postgres standby instances. Optional.
* **enableReplicaPoolerLoadBalancer**
boolean flag to override the operator defaults (set by the
`enable_replica_pooler_load_balancer` parameter) to define whether to enable
the load balancer for replica pooler pods pointing to the Postgres standby
instances. Optional.
* **allowedSourceRanges** * **allowedSourceRanges**
when one or more load balancers are enabled for the cluster, this parameter when one or more load balancers are enabled for the cluster, this parameter
defines the comma-separated range of IP networks (in CIDR-notation). The defines the comma-separated range of IP networks (in CIDR-notation). The
@@ -310,9 +322,7 @@ explanation of `ttl` and `loop_wait` parameters.
Those parameters define [CPU and memory requests and limits](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) Those parameters define [CPU and memory requests and limits](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/)
for the Postgres container. They are grouped under the `resources` top-level for the Postgres container. They are grouped under the `resources` top-level
key with subgroups `requests` and `limits`. The whole section is optional, key with subgroups `requests` and `limits`.
however if you specify a request or limit you have to define everything
(unless you are not modifying the default CRD schema validation).
### Requests ### Requests
+44 -7
View File
@@ -159,9 +159,9 @@ Those are top-level keys, containing both leaf keys and groups.
at the cost of overprovisioning memory and potential scheduling problems for at the cost of overprovisioning memory and potential scheduling problems for
containers with high memory limits due to the lack of memory on Kubernetes containers with high memory limits due to the lack of memory on Kubernetes
cluster nodes. This affects all containers created by the operator (Postgres, cluster nodes. This affects all containers created by the operator (Postgres,
Scalyr sidecar, and other sidecars except **sidecars** defined in the operator connection pooler, logical backup, scalyr sidecar, and other sidecars except
configuration); to set resources for the operator's own container, change the **sidecars** defined in the operator configuration); to set resources for the
[operator deployment manually](https://github.com/zalando/postgres-operator/blob/master/manifests/postgres-operator.yaml#L20). operator's own container, change the [operator deployment manually](https://github.com/zalando/postgres-operator/blob/master/manifests/postgres-operator.yaml#L20).
The default is `false`. The default is `false`.
## Postgres users ## Postgres users
@@ -177,6 +177,15 @@ under the `users` key.
Postgres username used for replication between instances. The default is Postgres username used for replication between instances. The default is
`standby`. `standby`.
* **additional_owner_roles**
Specifies database roles that will become members of all database owners.
Then owners can use `SET ROLE` to obtain privileges of these roles to e.g.
create/update functionality from extensions as part of a migration script.
Note, that roles listed here should be preconfigured in the docker image
and already exist in the database cluster on startup. One such role can be
`cron_admin` which is provided by the Spilo docker image to set up cron
jobs inside the `postgres` database. Default is `empty`.
* **enable_password_rotation** * **enable_password_rotation**
For all `LOGIN` roles that are not database owners the operator can rotate For all `LOGIN` roles that are not database owners the operator can rotate
credentials in the corresponding K8s secrets by replacing the username and credentials in the corresponding K8s secrets by replacing the username and
@@ -278,6 +287,12 @@ configuration they are grouped under the `kubernetes` key.
Regular expressions like `downscaler/*` etc. are also accepted. Can be used Regular expressions like `downscaler/*` etc. are also accepted. Can be used
with [kube-downscaler](https://github.com/hjacobs/kube-downscaler). with [kube-downscaler](https://github.com/hjacobs/kube-downscaler).
* **ignored_annotations**
Some K8s tools inject and update annotations out of the Postgres Operator
control. This can cause rolling updates on each cluster sync cycle. With
this option you can specify an array of annotation keys that should be
ignored when comparing K8s resources on sync. The default is empty.
* **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 `*`
@@ -498,6 +513,13 @@ configuration `resource_check_interval` and `resource_check_timeout` have no
effect, and the parameters are grouped under the `timeouts` key in the effect, and the parameters are grouped under the `timeouts` key in the
CRD-based configuration. CRD-based configuration.
* **PatroniAPICheckInterval**
the interval between consecutive attempts waiting for the return of
Patroni Api. The default is `1s`.
* **PatroniAPICheckTimeout**
the timeout for a response from Patroni Api. The default is `5s`.
* **resource_check_interval** * **resource_check_interval**
interval to wait between consecutive attempts to check for the presence of interval to wait between consecutive attempts to check for the presence of
some Kubernetes resource (i.e. `StatefulSet` or `PodDisruptionBudget`). The some Kubernetes resource (i.e. `StatefulSet` or `PodDisruptionBudget`). The
@@ -546,11 +568,21 @@ In the CRD-based configuration they are grouped under the `load_balancer` key.
toggles service type load balancer pointing to the master pod of the cluster. toggles service type load balancer pointing to the master pod of the cluster.
Can be overridden by individual cluster settings. The default is `true`. Can be overridden by individual cluster settings. The default is `true`.
* **enable_replica_load_balancer** * **enable_master_pooler_load_balancer**
toggles service type load balancer pointing to the replica pod of the toggles service type load balancer pointing to the master pooler pod of the
cluster. Can be overridden by individual cluster settings. The default is cluster. Can be overridden by individual cluster settings. The default is
`false`. `false`.
* **enable_replica_load_balancer**
toggles service type load balancer pointing to the replica pod(s) of the
cluster. Can be overridden by individual cluster settings. The default is
`false`.
* **enable_replica_pooler_load_balancer**
toggles service type load balancer pointing to the replica pooler pod(s) of
the cluster. Can be overridden by individual cluster settings. The default
is `false`.
* **external_traffic_policy** defines external traffic policy for load * **external_traffic_policy** defines external traffic policy for load
balancers. Allowed values are `Cluster` (default) and `Local`. balancers. Allowed values are `Cluster` (default) and `Local`.
@@ -676,6 +708,11 @@ grouped under the `logical_backup` key.
Specify server side encryption that S3 storage is using. If empty string Specify server side encryption that S3 storage is using. If empty string
is specified, no argument will be passed to `aws s3` command. Default: "AES256". is specified, no argument will be passed to `aws s3` command. Default: "AES256".
* **logical_backup_s3_retention_time**
Specify a retention time for logical backups stored in S3. Backups older than the specified retention
time will be deleted after a new backup was uploaded. If empty, all backups will be kept. Example values are
"3 days", "2 weeks", or "1 month". The default is empty.
* **logical_backup_schedule** * **logical_backup_schedule**
Backup schedule in the cron format. Please take the Backup schedule in the cron format. Please take the
[reference schedule format](https://kubernetes.io/docs/tasks/job/automated-tasks-with-cron-jobs/#schedule) [reference schedule format](https://kubernetes.io/docs/tasks/job/automated-tasks-with-cron-jobs/#schedule)
@@ -748,7 +785,7 @@ key.
* **protected_role_names** * **protected_role_names**
List of roles that cannot be overwritten by an application, team or List of roles that cannot be overwritten by an application, team or
infrastructure role. The default is `admin`. infrastructure role. The default list is `admin` and `cron_admin`.
* **postgres_superuser_teams** * **postgres_superuser_teams**
List of teams which members need the superuser role in each PG database List of teams which members need the superuser role in each PG database
+131 -43
View File
@@ -158,6 +158,37 @@ class EndToEndTestCase(unittest.TestCase):
print('Operator log: {}'.format(k8s.get_operator_log())) print('Operator log: {}'.format(k8s.get_operator_log()))
raise raise
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_additional_owner_roles(self):
'''
Test adding additional member roles to existing database owner roles
'''
k8s = self.k8s
# enable PostgresTeam CRD and lower resync
owner_roles = {
"data": {
"additional_owner_roles": "cron_admin",
},
}
k8s.update_config(owner_roles)
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"},
"Operator does not get in sync")
leader = k8s.get_cluster_leader_pod()
owner_query = """
SELECT a2.rolname
FROM pg_catalog.pg_authid a
JOIN pg_catalog.pg_auth_members am
ON a.oid = am.member
AND a.rolname = 'cron_admin'
JOIN pg_catalog.pg_authid a2
ON a2.oid = am.roleid
WHERE a2.rolname IN ('zalando', 'bar_owner', 'bar_data_owner');
"""
self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", owner_query)), 3,
"Not all additional users found in database", 10, 5)
@timeout_decorator.timeout(TEST_TIMEOUT_SEC) @timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_additional_pod_capabilities(self): def test_additional_pod_capabilities(self):
''' '''
@@ -177,13 +208,12 @@ class EndToEndTestCase(unittest.TestCase):
try: try:
k8s.update_config(patch_capabilities) k8s.update_config(patch_capabilities)
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"},
"Operator does not get in sync")
# changed security context of postgres container should trigger a rolling update # changed security context of postgres container should trigger a rolling update
k8s.wait_for_pod_failover(replica_nodes, 'spilo-role=master,' + cluster_label) k8s.wait_for_pod_failover(replica_nodes, 'spilo-role=master,' + cluster_label)
k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label) k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label)
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
self.eventuallyEqual(lambda: k8s.count_pods_with_container_capabilities(capabilities, cluster_label), self.eventuallyEqual(lambda: k8s.count_pods_with_container_capabilities(capabilities, cluster_label),
2, "Container capabilities not updated") 2, "Container capabilities not updated")
@@ -209,8 +239,6 @@ class EndToEndTestCase(unittest.TestCase):
}, },
} }
k8s.update_config(enable_postgres_team_crd) k8s.update_config(enable_postgres_team_crd)
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"},
"Operator does not get in sync")
k8s.api.custom_objects_api.patch_namespaced_custom_object( k8s.api.custom_objects_api.patch_namespaced_custom_object(
'acid.zalan.do', 'v1', 'default', 'acid.zalan.do', 'v1', 'default',
@@ -335,7 +363,7 @@ class EndToEndTestCase(unittest.TestCase):
try: try:
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_config) "acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_config)
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
def compare_config(): def compare_config():
@@ -452,7 +480,7 @@ class EndToEndTestCase(unittest.TestCase):
} }
} }
}) })
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"},
"Operator does not get in sync") "Operator does not get in sync")
self.eventuallyEqual(lambda: k8s.count_secrets_with_label("cluster-name=acid-minimal-cluster,application=spilo", self.test_namespace), self.eventuallyEqual(lambda: k8s.count_secrets_with_label("cluster-name=acid-minimal-cluster,application=spilo", self.test_namespace),
@@ -467,6 +495,9 @@ class EndToEndTestCase(unittest.TestCase):
the end turn connection pooler off to not interfere with other tests. the end turn connection pooler off to not interfere with other tests.
''' '''
k8s = self.k8s k8s = self.k8s
pooler_label = 'application=db-connection-pooler,cluster-name=acid-minimal-cluster'
master_pooler_label = 'connection-pooler=acid-minimal-cluster-pooler'
replica_pooler_label = master_pooler_label + '-repl'
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
k8s.api.custom_objects_api.patch_namespaced_custom_object( k8s.api.custom_objects_api.patch_namespaced_custom_object(
@@ -478,20 +509,30 @@ class EndToEndTestCase(unittest.TestCase):
'enableReplicaConnectionPooler': True, 'enableReplicaConnectionPooler': True,
} }
}) })
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
self.eventuallyEqual(lambda: k8s.get_deployment_replica_count(), 2, self.eventuallyEqual(lambda: k8s.get_deployment_replica_count(), 2, "Deployment replicas is 2 default")
"Deployment replicas is 2 default") self.eventuallyEqual(lambda: k8s.count_running_pods(master_pooler_label), 2, "No pooler pods found")
self.eventuallyEqual(lambda: k8s.count_running_pods( self.eventuallyEqual(lambda: k8s.count_running_pods(replica_pooler_label), 2, "No pooler replica pods found")
"connection-pooler=acid-minimal-cluster-pooler"), self.eventuallyEqual(lambda: k8s.count_services_with_label(pooler_label), 2, "No pooler service found")
2, "No pooler pods found") self.eventuallyEqual(lambda: k8s.count_secrets_with_label(pooler_label), 1, "Pooler secret not created")
self.eventuallyEqual(lambda: k8s.count_running_pods(
"connection-pooler=acid-minimal-cluster-pooler-repl"), k8s.api.custom_objects_api.patch_namespaced_custom_object(
2, "No pooler replica pods found") 'acid.zalan.do', 'v1', 'default',
self.eventuallyEqual(lambda: k8s.count_services_with_label( 'postgresqls', 'acid-minimal-cluster',
'application=db-connection-pooler,cluster-name=acid-minimal-cluster'), {
2, "No pooler service found") 'spec': {
self.eventuallyEqual(lambda: k8s.count_secrets_with_label('application=db-connection-pooler,cluster-name=acid-minimal-cluster'), 'enableMasterPoolerLoadBalancer': True,
1, "Pooler secret not created") 'enableReplicaPoolerLoadBalancer': True,
}
})
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
self.eventuallyEqual(lambda: k8s.get_service_type(master_pooler_label+","+pooler_label),
'LoadBalancer',
"Expected LoadBalancer service type for master pooler pod, found {}")
self.eventuallyEqual(lambda: k8s.get_service_type(replica_pooler_label+","+pooler_label),
'LoadBalancer',
"Expected LoadBalancer service type for replica pooler pod, found {}")
# Turn off only master connection pooler # Turn off only master connection pooler
k8s.api.custom_objects_api.patch_namespaced_custom_object( k8s.api.custom_objects_api.patch_namespaced_custom_object(
@@ -504,20 +545,17 @@ class EndToEndTestCase(unittest.TestCase):
} }
}) })
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
"Operator does not get in sync")
self.eventuallyEqual(lambda: k8s.get_deployment_replica_count(name="acid-minimal-cluster-pooler-repl"), 2, self.eventuallyEqual(lambda: k8s.get_deployment_replica_count(name="acid-minimal-cluster-pooler-repl"), 2,
"Deployment replicas is 2 default") "Deployment replicas is 2 default")
self.eventuallyEqual(lambda: k8s.count_running_pods( self.eventuallyEqual(lambda: k8s.count_running_pods(master_pooler_label),
"connection-pooler=acid-minimal-cluster-pooler"),
0, "Master pooler pods not deleted") 0, "Master pooler pods not deleted")
self.eventuallyEqual(lambda: k8s.count_running_pods( self.eventuallyEqual(lambda: k8s.count_running_pods(replica_pooler_label),
"connection-pooler=acid-minimal-cluster-pooler-repl"),
2, "Pooler replica pods not found") 2, "Pooler replica pods not found")
self.eventuallyEqual(lambda: k8s.count_services_with_label( self.eventuallyEqual(lambda: k8s.count_services_with_label(pooler_label),
'application=db-connection-pooler,cluster-name=acid-minimal-cluster'),
1, "No pooler service found") 1, "No pooler service found")
self.eventuallyEqual(lambda: k8s.count_secrets_with_label('application=db-connection-pooler,cluster-name=acid-minimal-cluster'), self.eventuallyEqual(lambda: k8s.count_secrets_with_label(pooler_label),
1, "Secret not created") 1, "Secret not created")
# Turn off only replica connection pooler # Turn off only replica connection pooler
@@ -528,20 +566,24 @@ class EndToEndTestCase(unittest.TestCase):
'spec': { 'spec': {
'enableConnectionPooler': True, 'enableConnectionPooler': True,
'enableReplicaConnectionPooler': False, 'enableReplicaConnectionPooler': False,
'enableMasterPoolerLoadBalancer': False,
} }
}) })
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
"Operator does not get in sync")
self.eventuallyEqual(lambda: k8s.get_deployment_replica_count(), 2, self.eventuallyEqual(lambda: k8s.get_deployment_replica_count(), 2,
"Deployment replicas is 2 default") "Deployment replicas is 2 default")
self.eventuallyEqual(lambda: k8s.count_running_pods("connection-pooler=acid-minimal-cluster-pooler"), self.eventuallyEqual(lambda: k8s.count_running_pods(master_pooler_label),
2, "Master pooler pods not found") 2, "Master pooler pods not found")
self.eventuallyEqual(lambda: k8s.count_running_pods("connection-pooler=acid-minimal-cluster-pooler-repl"), self.eventuallyEqual(lambda: k8s.count_running_pods(replica_pooler_label),
0, "Pooler replica pods not deleted") 0, "Pooler replica pods not deleted")
self.eventuallyEqual(lambda: k8s.count_services_with_label('application=db-connection-pooler,cluster-name=acid-minimal-cluster'), self.eventuallyEqual(lambda: k8s.count_services_with_label(pooler_label),
1, "No pooler service found") 1, "No pooler service found")
self.eventuallyEqual(lambda: k8s.count_secrets_with_label('application=db-connection-pooler,cluster-name=acid-minimal-cluster'), self.eventuallyEqual(lambda: k8s.get_service_type(master_pooler_label+","+pooler_label),
'ClusterIP',
"Expected LoadBalancer service type for master, found {}")
self.eventuallyEqual(lambda: k8s.count_secrets_with_label(pooler_label),
1, "Secret not created") 1, "Secret not created")
# scale up connection pooler deployment # scale up connection pooler deployment
@@ -558,7 +600,7 @@ class EndToEndTestCase(unittest.TestCase):
self.eventuallyEqual(lambda: k8s.get_deployment_replica_count(), 3, self.eventuallyEqual(lambda: k8s.get_deployment_replica_count(), 3,
"Deployment replicas is scaled to 3") "Deployment replicas is scaled to 3")
self.eventuallyEqual(lambda: k8s.count_running_pods("connection-pooler=acid-minimal-cluster-pooler"), self.eventuallyEqual(lambda: k8s.count_running_pods(master_pooler_label),
3, "Scale up of pooler pods does not work") 3, "Scale up of pooler pods does not work")
# turn it off, keeping config should be overwritten by false # turn it off, keeping config should be overwritten by false
@@ -569,12 +611,13 @@ class EndToEndTestCase(unittest.TestCase):
'spec': { 'spec': {
'enableConnectionPooler': False, 'enableConnectionPooler': False,
'enableReplicaConnectionPooler': False, 'enableReplicaConnectionPooler': False,
'enableReplicaPoolerLoadBalancer': False,
} }
}) })
self.eventuallyEqual(lambda: k8s.count_running_pods("connection-pooler=acid-minimal-cluster-pooler"), self.eventuallyEqual(lambda: k8s.count_running_pods(master_pooler_label),
0, "Pooler pods not scaled down") 0, "Pooler pods not scaled down")
self.eventuallyEqual(lambda: k8s.count_services_with_label('application=db-connection-pooler,cluster-name=acid-minimal-cluster'), self.eventuallyEqual(lambda: k8s.count_services_with_label(pooler_label),
0, "Pooler service not removed") 0, "Pooler service not removed")
self.eventuallyEqual(lambda: k8s.count_secrets_with_label('application=spilo,cluster-name=acid-minimal-cluster'), self.eventuallyEqual(lambda: k8s.count_secrets_with_label('application=spilo,cluster-name=acid-minimal-cluster'),
4, "Secrets not deleted") 4, "Secrets not deleted")
@@ -661,6 +704,49 @@ class EndToEndTestCase(unittest.TestCase):
print('Operator log: {}'.format(k8s.get_operator_log())) print('Operator log: {}'.format(k8s.get_operator_log()))
raise raise
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_ignored_annotations(self):
'''
Test if injected annotation does not cause replacement of resources when listed under ignored_annotations
'''
k8s = self.k8s
annotation_patch = {
"metadata": {
"annotations": {
"k8s-status": "healthy"
},
}
}
try:
sts = k8s.api.apps_v1.read_namespaced_stateful_set('acid-minimal-cluster', 'default')
old_sts_creation_timestamp = sts.metadata.creation_timestamp
k8s.api.apps_v1.patch_namespaced_stateful_set(sts.metadata.name, sts.metadata.namespace, annotation_patch)
svc = k8s.api.core_v1.read_namespaced_service('acid-minimal-cluster', 'default')
old_svc_creation_timestamp = svc.metadata.creation_timestamp
k8s.api.core_v1.patch_namespaced_service(svc.metadata.name, svc.metadata.namespace, annotation_patch)
patch_config_ignored_annotations = {
"data": {
"ignored_annotations": "k8s-status",
}
}
k8s.update_config(patch_config_ignored_annotations)
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
sts = k8s.api.apps_v1.read_namespaced_stateful_set('acid-minimal-cluster', 'default')
new_sts_creation_timestamp = sts.metadata.creation_timestamp
svc = k8s.api.core_v1.read_namespaced_service('acid-minimal-cluster', 'default')
new_svc_creation_timestamp = svc.metadata.creation_timestamp
self.assertEqual(old_sts_creation_timestamp, new_sts_creation_timestamp, "unexpected replacement of statefulset on sync")
self.assertEqual(old_svc_creation_timestamp, new_svc_creation_timestamp, "unexpected replacement of master service on sync")
except timeout_decorator.TimeoutError:
print('Operator log: {}'.format(k8s.get_operator_log()))
raise
@timeout_decorator.timeout(TEST_TIMEOUT_SEC) @timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_infrastructure_roles(self): def test_infrastructure_roles(self):
''' '''
@@ -956,12 +1042,12 @@ class EndToEndTestCase(unittest.TestCase):
} }
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_resources) "acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_resources)
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"},
"Operator does not get in sync")
# wait for switched over # wait for switched over
k8s.wait_for_pod_failover(replica_nodes, 'spilo-role=master,' + cluster_label) k8s.wait_for_pod_failover(replica_nodes, 'spilo-role=master,' + cluster_label)
k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label) k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label)
self.eventuallyEqual(lambda: len(k8s.get_patroni_running_members()), 2, "Postgres status did not enter running")
def verify_pod_limits(): def verify_pod_limits():
pods = k8s.api.core_v1.list_namespaced_pod('default', label_selector="cluster-name=acid-minimal-cluster,application=spilo").items pods = k8s.api.core_v1.list_namespaced_pod('default', label_selector="cluster-name=acid-minimal-cluster,application=spilo").items
@@ -1063,7 +1149,8 @@ class EndToEndTestCase(unittest.TestCase):
plural="postgresqls", plural="postgresqls",
name="acid-minimal-cluster", name="acid-minimal-cluster",
body=patch_node_affinity_config) body=patch_node_affinity_config)
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"},
"Operator does not get in sync")
# node affinity change should cause replica to relocate from replica node to master node due to node affinity requirement # node affinity change should cause replica to relocate from replica node to master node due to node affinity requirement
k8s.wait_for_pod_failover(master_nodes, 'spilo-role=replica,' + cluster_label) k8s.wait_for_pod_failover(master_nodes, 'spilo-role=replica,' + cluster_label)
@@ -1177,10 +1264,11 @@ class EndToEndTestCase(unittest.TestCase):
@timeout_decorator.timeout(TEST_TIMEOUT_SEC) @timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_overwrite_pooler_deployment(self): def test_overwrite_pooler_deployment(self):
pooler_name = 'acid-minimal-cluster-pooler'
k8s = self.k8s k8s = self.k8s
k8s.create_with_kubectl("manifests/minimal-fake-pooler-deployment.yaml") k8s.create_with_kubectl("manifests/minimal-fake-pooler-deployment.yaml")
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
self.eventuallyEqual(lambda: k8s.get_deployment_replica_count(name="acid-minimal-cluster-pooler"), 1, self.eventuallyEqual(lambda: k8s.get_deployment_replica_count(name=pooler_name), 1,
"Initial broken deployment not rolled out") "Initial broken deployment not rolled out")
k8s.api.custom_objects_api.patch_namespaced_custom_object( k8s.api.custom_objects_api.patch_namespaced_custom_object(
@@ -1193,7 +1281,7 @@ class EndToEndTestCase(unittest.TestCase):
}) })
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
self.eventuallyEqual(lambda: k8s.get_deployment_replica_count(name="acid-minimal-cluster-pooler"), 2, self.eventuallyEqual(lambda: k8s.get_deployment_replica_count(name=pooler_name), 2,
"Operator did not succeed in overwriting labels") "Operator did not succeed in overwriting labels")
k8s.api.custom_objects_api.patch_namespaced_custom_object( k8s.api.custom_objects_api.patch_namespaced_custom_object(
@@ -1206,7 +1294,7 @@ class EndToEndTestCase(unittest.TestCase):
}) })
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
self.eventuallyEqual(lambda: k8s.count_running_pods("connection-pooler=acid-minimal-cluster-pooler"), self.eventuallyEqual(lambda: k8s.count_running_pods("connection-pooler="+pooler_name),
0, "Pooler pods not scaled down") 0, "Pooler pods not scaled down")
@timeout_decorator.timeout(TEST_TIMEOUT_SEC) @timeout_decorator.timeout(TEST_TIMEOUT_SEC)
+8 -1
View File
@@ -3,6 +3,7 @@ kind: ConfigMap
metadata: metadata:
name: postgres-operator name: postgres-operator
data: data:
# additional_owner_roles: "cron_admin"
# additional_pod_capabilities: "SYS_NICE" # additional_pod_capabilities: "SYS_NICE"
# additional_secret_mount: "some-secret-name" # additional_secret_mount: "some-secret-name"
# additional_secret_mount_path: "/some/dir" # additional_secret_mount_path: "/some/dir"
@@ -44,6 +45,7 @@ data:
# enable_init_containers: "true" # enable_init_containers: "true"
# enable_lazy_spilo_upgrade: "false" # enable_lazy_spilo_upgrade: "false"
enable_master_load_balancer: "false" enable_master_load_balancer: "false"
enable_master_pooler_load_balancer: "false"
enable_password_rotation: "false" enable_password_rotation: "false"
enable_pgversion_env_var: "true" enable_pgversion_env_var: "true"
# enable_pod_antiaffinity: "false" # enable_pod_antiaffinity: "false"
@@ -51,6 +53,7 @@ data:
# enable_postgres_team_crd: "false" # enable_postgres_team_crd: "false"
# enable_postgres_team_crd_superusers: "false" # enable_postgres_team_crd_superusers: "false"
enable_replica_load_balancer: "false" enable_replica_load_balancer: "false"
enable_replica_pooler_load_balancer: "false"
# enable_shm_volume: "true" # enable_shm_volume: "true"
# enable_sidecars: "true" # enable_sidecars: "true"
enable_spilo_wal_path_compat: "true" enable_spilo_wal_path_compat: "true"
@@ -61,6 +64,7 @@ data:
external_traffic_policy: "Cluster" external_traffic_policy: "Cluster"
# gcp_credentials: "" # gcp_credentials: ""
# kubernetes_use_configmaps: "false" # kubernetes_use_configmaps: "false"
# ignored_annotations: ""
# infrastructure_roles_secret_name: "postgresql-infrastructure-roles" # infrastructure_roles_secret_name: "postgresql-infrastructure-roles"
# infrastructure_roles_secrets: "secretname:monitoring-roles,userkey:user,passwordkey:password,rolekey:inrole" # infrastructure_roles_secrets: "secretname:monitoring-roles,userkey:user,passwordkey:password,rolekey:inrole"
# inherited_annotations: owned-by # inherited_annotations: owned-by
@@ -77,6 +81,7 @@ data:
# 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_s3_retention_time: ""
logical_backup_schedule: "30 00 * * *" logical_backup_schedule: "30 00 * * *"
major_version_upgrade_mode: "manual" major_version_upgrade_mode: "manual"
# major_version_upgrade_team_allow_list: "" # major_version_upgrade_team_allow_list: ""
@@ -93,6 +98,8 @@ data:
# pam_configuration: | # pam_configuration: |
# https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees # https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees
# pam_role_name: zalandos # pam_role_name: zalandos
patroni_api_check_interval: "1s"
patroni_api_check_timeout: "5s"
# password_rotation_interval: "90" # password_rotation_interval: "90"
# password_rotation_user_retention: "180" # password_rotation_user_retention: "180"
pdb_name_format: "postgres-{cluster}-pdb" pdb_name_format: "postgres-{cluster}-pdb"
@@ -109,7 +116,7 @@ data:
# pod_service_account_role_binding_definition: "" # pod_service_account_role_binding_definition: ""
pod_terminate_grace_period: 5m pod_terminate_grace_period: 5m
# postgres_superuser_teams: "postgres_superusers" # postgres_superuser_teams: "postgres_superusers"
# protected_role_names: "admin" # protected_role_names: "admin,cron_admin"
ready_wait_interval: 3s ready_wait_interval: 3s
ready_wait_timeout: 30s ready_wait_timeout: 30s
repair_period: 5m repair_period: 5m
@@ -0,0 +1,131 @@
# Here we use https://github.com/prometheus-community/helm-charts/charts/kube-prometheus-stack
# Please keep the ServiceMonitor's label same as the Helm release name of kube-prometheus-stack
apiVersion: v1
kind: Namespace
metadata:
name: test-pg
---
apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
name: acid-minimal-cluster
namespace: test-pg
labels:
app: test-pg
spec:
teamId: "acid"
volume:
size: 1Gi
numberOfInstances: 2
users:
zalando: # database owner
- superuser
- createdb
foo_user: [] # role for application foo
databases:
foo: zalando # dbname: owner
preparedDatabases:
bar: {}
postgresql:
version: "13"
sidecars:
- name: "exporter"
image: "wrouesnel/postgres_exporter"
ports:
- name: exporter
containerPort: 9187
protocol: TCP
resources:
limits:
cpu: 500m
memory: 256M
requests:
cpu: 100m
memory: 200M
---
apiVersion: v1
kind: Service
metadata:
name: acid-minimal-cluster-svc-metrics-master
namespace: test-pg
labels:
app: test-pg
spilo-role: master
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9187"
spec:
type: ClusterIP
ports:
- name: exporter
port: 9187
targetPort: exporter
selector:
application: spilo
cluster-name: acid-minimal-cluster
spilo-role: master
---
apiVersion: v1
kind: Service
metadata:
name: acid-minimal-cluster-svc-metrics-replica
namespace: test-pg
labels:
app: test-pg
spilo-role: replica
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9187"
spec:
type: ClusterIP
ports:
- name: exporter
port: 9187
targetPort: exporter
selector:
application: spilo
cluster-name: acid-minimal-cluster
spilo-role: replica
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: acid-minimal-cluster-svcm-master
namespace: test-pg
labels:
app: test-pg
spilo-role: master
spec:
endpoints:
- port: exporter
interval: 15s
scrapeTimeout: 10s
namespaceSelector:
matchNames:
- test-pg
selector:
matchLabels:
app: test-pg
spilo-role: master
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: acid-minimal-cluster-svcm-replica
namespace: test-pg
labels:
app: test-pg
spilo-role: replica
spec:
endpoints:
- port: exporter
interval: 15s
scrapeTimeout: 10s
namespaceSelector:
matchNames:
- test-pg
selector:
matchLabels:
app: test-pg
spilo-role: replica
+24
View File
@@ -128,6 +128,11 @@ spec:
users: users:
type: object type: object
properties: properties:
additional_owner_roles:
type: array
nullable: true
items:
type: string
enable_password_rotation: enable_password_rotation:
type: boolean type: boolean
default: false default: false
@@ -205,6 +210,10 @@ spec:
enable_sidecars: enable_sidecars:
type: boolean type: boolean
default: true default: true
ignored_annotations:
type: array
items:
type: string
infrastructure_roles_secret_name: infrastructure_roles_secret_name:
type: string type: string
infrastructure_roles_secrets: infrastructure_roles_secrets:
@@ -347,6 +356,12 @@ spec:
timeouts: timeouts:
type: object type: object
properties: properties:
patroni_api_check_interval:
type: string
default: "1s"
patroni_api_check_timeout:
type: string
default: "5s"
pod_label_wait_timeout: pod_label_wait_timeout:
type: string type: string
default: "10m" default: "10m"
@@ -378,9 +393,15 @@ spec:
enable_master_load_balancer: enable_master_load_balancer:
type: boolean type: boolean
default: true default: true
enable_master_pooler_load_balancer:
type: boolean
default: false
enable_replica_load_balancer: enable_replica_load_balancer:
type: boolean type: boolean
default: false default: false
enable_replica_pooler_load_balancer:
type: boolean
default: false
external_traffic_policy: external_traffic_policy:
type: string type: string
enum: enum:
@@ -448,6 +469,8 @@ spec:
type: string type: string
logical_backup_s3_sse: logical_backup_s3_sse:
type: string type: string
logical_backup_s3_retention_time:
type: string
logical_backup_schedule: logical_backup_schedule:
type: string type: string
pattern: '^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$' pattern: '^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$'
@@ -498,6 +521,7 @@ spec:
type: string type: string
default: default:
- admin - admin
- cron_admin
role_deletion_suffix: role_deletion_suffix:
type: string type: string
default: "_deleted" default: "_deleted"
@@ -26,6 +26,8 @@ configuration:
# protocol: TCP # protocol: TCP
workers: 8 workers: 8
users: users:
# additional_owner_roles:
# - cron_admin
enable_password_rotation: false enable_password_rotation: false
password_rotation_interval: 90 password_rotation_interval: 90
password_rotation_user_retention: 180 password_rotation_user_retention: 180
@@ -57,6 +59,8 @@ configuration:
enable_pod_antiaffinity: false enable_pod_antiaffinity: false
enable_pod_disruption_budget: true enable_pod_disruption_budget: true
enable_sidecars: true enable_sidecars: true
# ignored_annotations:
# - k8s.v1.cni.cncf.io/network-status
# infrastructure_roles_secret_name: "postgresql-infrastructure-roles" # infrastructure_roles_secret_name: "postgresql-infrastructure-roles"
# infrastructure_roles_secrets: # infrastructure_roles_secrets:
# - secretname: "monitoring-roles" # - secretname: "monitoring-roles"
@@ -107,6 +111,8 @@ configuration:
# min_cpu_limit: 250m # min_cpu_limit: 250m
# min_memory_limit: 250Mi # min_memory_limit: 250Mi
timeouts: timeouts:
patroni_api_check_interval: 1s
patroni_api_check_timeout: 5s
pod_label_wait_timeout: 10m pod_label_wait_timeout: 10m
pod_deletion_wait_timeout: 10m pod_deletion_wait_timeout: 10m
ready_wait_interval: 4s ready_wait_interval: 4s
@@ -119,7 +125,9 @@ configuration:
# keyy: valuey # keyy: valuey
# db_hosted_zone: "" # db_hosted_zone: ""
enable_master_load_balancer: false enable_master_load_balancer: false
enable_master_pooler_load_balancer: false
enable_replica_load_balancer: false enable_replica_load_balancer: false
enable_replica_pooler_load_balancer: false
external_traffic_policy: "Cluster" external_traffic_policy: "Cluster"
master_dns_name_format: "{cluster}.{team}.{hostedzone}" master_dns_name_format: "{cluster}.{team}.{hostedzone}"
replica_dns_name_format: "{cluster}-repl.{team}.{hostedzone}" replica_dns_name_format: "{cluster}-repl.{team}.{hostedzone}"
@@ -146,6 +154,7 @@ configuration:
# logical_backup_s3_region: "" # logical_backup_s3_region: ""
# logical_backup_s3_secret_access_key: "" # logical_backup_s3_secret_access_key: ""
logical_backup_s3_sse: "AES256" logical_backup_s3_sse: "AES256"
# logical_backup_s3_retention_time: ""
logical_backup_schedule: "30 00 * * *" logical_backup_schedule: "30 00 * * *"
debug: debug:
debug_logging: true debug_logging: true
@@ -163,6 +172,7 @@ configuration:
# - postgres_superusers # - postgres_superusers
protected_role_names: protected_role_names:
- admin - admin
- cron_admin
role_deletion_suffix: "_deleted" role_deletion_suffix: "_deleted"
team_admin_role: admin team_admin_role: admin
team_api_role_configuration: team_api_role_configuration:
+4 -22
View File
@@ -148,15 +148,9 @@ spec:
minimum: 1 minimum: 1
resources: resources:
type: object type: object
required:
- requests
- limits
properties: properties:
limits: limits:
type: object type: object
required:
- cpu
- memory
properties: properties:
cpu: cpu:
type: string type: string
@@ -166,9 +160,6 @@ spec:
pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$'
requests: requests:
type: object type: object
required:
- cpu
- memory
properties: properties:
cpu: cpu:
type: string type: string
@@ -195,8 +186,12 @@ spec:
type: boolean type: boolean
enableMasterLoadBalancer: enableMasterLoadBalancer:
type: boolean type: boolean
enableMasterPoolerLoadBalancer:
type: boolean
enableReplicaLoadBalancer: enableReplicaLoadBalancer:
type: boolean type: boolean
enableReplicaPoolerLoadBalancer:
type: boolean
enableShmVolume: enableShmVolume:
type: boolean type: boolean
init_containers: init_containers:
@@ -400,15 +395,9 @@ spec:
description: deprecated description: deprecated
resources: resources:
type: object type: object
required:
- requests
- limits
properties: properties:
limits: limits:
type: object type: object
required:
- cpu
- memory
properties: properties:
cpu: cpu:
type: string type: string
@@ -437,9 +426,6 @@ spec:
# than the corresponding limit. # than the corresponding limit.
requests: requests:
type: object type: object
required:
- cpu
- memory
properties: properties:
cpu: cpu:
type: string type: string
@@ -530,10 +516,6 @@ spec:
type: array type: array
items: items:
type: object type: object
required:
- key
- operator
- effect
properties: properties:
key: key:
type: string type: string
+54 -14
View File
@@ -238,12 +238,10 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
Minimum: &min1, Minimum: &min1,
}, },
"resources": { "resources": {
Type: "object", Type: "object",
Required: []string{"requests", "limits"},
Properties: map[string]apiextv1.JSONSchemaProps{ Properties: map[string]apiextv1.JSONSchemaProps{
"limits": { "limits": {
Type: "object", Type: "object",
Required: []string{"cpu", "memory"},
Properties: map[string]apiextv1.JSONSchemaProps{ Properties: map[string]apiextv1.JSONSchemaProps{
"cpu": { "cpu": {
Type: "string", Type: "string",
@@ -256,8 +254,7 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
}, },
}, },
"requests": { "requests": {
Type: "object", Type: "object",
Required: []string{"cpu", "memory"},
Properties: map[string]apiextv1.JSONSchemaProps{ Properties: map[string]apiextv1.JSONSchemaProps{
"cpu": { "cpu": {
Type: "string", Type: "string",
@@ -302,9 +299,15 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
"enableMasterLoadBalancer": { "enableMasterLoadBalancer": {
Type: "boolean", Type: "boolean",
}, },
"enableMasterPoolerLoadBalancer": {
Type: "boolean",
},
"enableReplicaLoadBalancer": { "enableReplicaLoadBalancer": {
Type: "boolean", Type: "boolean",
}, },
"enableReplicaPoolerLoadBalancer": {
Type: "boolean",
},
"enableShmVolume": { "enableShmVolume": {
Type: "boolean", Type: "boolean",
}, },
@@ -642,12 +645,10 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
Description: "deprecated", Description: "deprecated",
}, },
"resources": { "resources": {
Type: "object", Type: "object",
Required: []string{"requests", "limits"},
Properties: map[string]apiextv1.JSONSchemaProps{ Properties: map[string]apiextv1.JSONSchemaProps{
"limits": { "limits": {
Type: "object", Type: "object",
Required: []string{"cpu", "memory"},
Properties: map[string]apiextv1.JSONSchemaProps{ Properties: map[string]apiextv1.JSONSchemaProps{
"cpu": { "cpu": {
Type: "string", Type: "string",
@@ -660,8 +661,7 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
}, },
}, },
"requests": { "requests": {
Type: "object", Type: "object",
Required: []string{"cpu", "memory"},
Properties: map[string]apiextv1.JSONSchemaProps{ Properties: map[string]apiextv1.JSONSchemaProps{
"cpu": { "cpu": {
Type: "string", Type: "string",
@@ -798,8 +798,7 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
Type: "array", Type: "array",
Items: &apiextv1.JSONSchemaPropsOrArray{ Items: &apiextv1.JSONSchemaPropsOrArray{
Schema: &apiextv1.JSONSchemaProps{ Schema: &apiextv1.JSONSchemaProps{
Type: "object", Type: "object",
Required: []string{"key", "operator", "effect"},
Properties: map[string]apiextv1.JSONSchemaProps{ Properties: map[string]apiextv1.JSONSchemaProps{
"key": { "key": {
Type: "string", Type: "string",
@@ -1148,6 +1147,24 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{
"users": { "users": {
Type: "object", Type: "object",
Properties: map[string]apiextv1.JSONSchemaProps{ Properties: map[string]apiextv1.JSONSchemaProps{
"additional_owner_roles": {
Type: "array",
Nullable: true,
Items: &apiextv1.JSONSchemaPropsOrArray{
Schema: &apiextv1.JSONSchemaProps{
Type: "string",
},
},
},
"enable_password_rotation": {
Type: "boolean",
},
"password_rotation_interval": {
Type: "integer",
},
"password_rotation_user_retention": {
Type: "integer",
},
"replication_username": { "replication_username": {
Type: "string", Type: "string",
}, },
@@ -1240,6 +1257,14 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{
"enable_sidecars": { "enable_sidecars": {
Type: "boolean", Type: "boolean",
}, },
"ignored_annotations": {
Type: "array",
Items: &apiextv1.JSONSchemaPropsOrArray{
Schema: &apiextv1.JSONSchemaProps{
Type: "string",
},
},
},
"infrastructure_roles_secret_name": { "infrastructure_roles_secret_name": {
Type: "string", Type: "string",
}, },
@@ -1438,6 +1463,12 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{
"timeouts": { "timeouts": {
Type: "object", Type: "object",
Properties: map[string]apiextv1.JSONSchemaProps{ Properties: map[string]apiextv1.JSONSchemaProps{
"patroni_api_check_interval": {
Type: "string",
},
"patroni_api_check_timeout": {
Type: "string",
},
"pod_label_wait_timeout": { "pod_label_wait_timeout": {
Type: "string", Type: "string",
}, },
@@ -1475,9 +1506,15 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{
"enable_master_load_balancer": { "enable_master_load_balancer": {
Type: "boolean", Type: "boolean",
}, },
"enable_master_pooler_load_balancer": {
Type: "boolean",
},
"enable_replica_load_balancer": { "enable_replica_load_balancer": {
Type: "boolean", Type: "boolean",
}, },
"enable_replica_pooler_load_balancer": {
Type: "boolean",
},
"external_traffic_policy": { "external_traffic_policy": {
Type: "string", Type: "string",
Enum: []apiextv1.JSON{ Enum: []apiextv1.JSON{
@@ -1562,6 +1599,9 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{
"logical_backup_s3_sse": { "logical_backup_s3_sse": {
Type: "string", Type: "string",
}, },
"logical_backup_s3_retention_time": {
Type: "string",
},
"logical_backup_schedule": { "logical_backup_schedule": {
Type: "string", Type: "string",
Pattern: "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$", Pattern: "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$",
@@ -37,11 +37,12 @@ type OperatorConfigurationList struct {
// PostgresUsersConfiguration defines the system users of Postgres. // PostgresUsersConfiguration defines the system users of Postgres.
type PostgresUsersConfiguration struct { type PostgresUsersConfiguration struct {
SuperUsername string `json:"super_username,omitempty"` SuperUsername string `json:"super_username,omitempty"`
ReplicationUsername string `json:"replication_username,omitempty"` ReplicationUsername string `json:"replication_username,omitempty"`
EnablePasswordRotation bool `json:"enable_password_rotation,omitempty"` AdditionalOwnerRoles []string `json:"additional_owner_roles,omitempty"`
PasswordRotationInterval uint32 `json:"password_rotation_interval,omitempty"` EnablePasswordRotation bool `json:"enable_password_rotation,omitempty"`
PasswordRotationUserRetention uint32 `json:"password_rotation_user_retention,omitempty"` PasswordRotationInterval uint32 `json:"password_rotation_interval,omitempty"`
PasswordRotationUserRetention uint32 `json:"password_rotation_user_retention,omitempty"`
} }
// MajorVersionUpgradeConfiguration defines how to execute major version upgrades of Postgres. // MajorVersionUpgradeConfiguration defines how to execute major version upgrades of Postgres.
@@ -81,6 +82,7 @@ type KubernetesMetaConfiguration struct {
InheritedLabels []string `json:"inherited_labels,omitempty"` InheritedLabels []string `json:"inherited_labels,omitempty"`
InheritedAnnotations []string `json:"inherited_annotations,omitempty"` InheritedAnnotations []string `json:"inherited_annotations,omitempty"`
DownscalerAnnotations []string `json:"downscaler_annotations,omitempty"` DownscalerAnnotations []string `json:"downscaler_annotations,omitempty"`
IgnoredAnnotations []string `json:"ignored_annotations,omitempty"`
ClusterNameLabel string `json:"cluster_name_label,omitempty"` ClusterNameLabel string `json:"cluster_name_label,omitempty"`
DeleteAnnotationDateKey string `json:"delete_annotation_date_key,omitempty"` DeleteAnnotationDateKey string `json:"delete_annotation_date_key,omitempty"`
DeleteAnnotationNameKey string `json:"delete_annotation_name_key,omitempty"` DeleteAnnotationNameKey string `json:"delete_annotation_name_key,omitempty"`
@@ -111,23 +113,27 @@ type PostgresPodResourcesDefaults struct {
// OperatorTimeouts defines the timeout of ResourceCheck, PodWait, ReadyWait // OperatorTimeouts defines the timeout of ResourceCheck, PodWait, ReadyWait
type OperatorTimeouts struct { type OperatorTimeouts struct {
ResourceCheckInterval Duration `json:"resource_check_interval,omitempty"` ResourceCheckInterval Duration `json:"resource_check_interval,omitempty"`
ResourceCheckTimeout Duration `json:"resource_check_timeout,omitempty"` ResourceCheckTimeout Duration `json:"resource_check_timeout,omitempty"`
PodLabelWaitTimeout Duration `json:"pod_label_wait_timeout,omitempty"` PodLabelWaitTimeout Duration `json:"pod_label_wait_timeout,omitempty"`
PodDeletionWaitTimeout Duration `json:"pod_deletion_wait_timeout,omitempty"` PodDeletionWaitTimeout Duration `json:"pod_deletion_wait_timeout,omitempty"`
ReadyWaitInterval Duration `json:"ready_wait_interval,omitempty"` ReadyWaitInterval Duration `json:"ready_wait_interval,omitempty"`
ReadyWaitTimeout Duration `json:"ready_wait_timeout,omitempty"` ReadyWaitTimeout Duration `json:"ready_wait_timeout,omitempty"`
PatroniAPICheckInterval Duration `json:"patroni_api_check_interval,omitempty"`
PatroniAPICheckTimeout Duration `json:"patroni_api_check_timeout,omitempty"`
} }
// LoadBalancerConfiguration defines the LB configuration // LoadBalancerConfiguration defines the LB configuration
type LoadBalancerConfiguration struct { type LoadBalancerConfiguration struct {
DbHostedZone string `json:"db_hosted_zone,omitempty"` DbHostedZone string `json:"db_hosted_zone,omitempty"`
EnableMasterLoadBalancer bool `json:"enable_master_load_balancer,omitempty"` EnableMasterLoadBalancer bool `json:"enable_master_load_balancer,omitempty"`
EnableReplicaLoadBalancer bool `json:"enable_replica_load_balancer,omitempty"` EnableMasterPoolerLoadBalancer bool `json:"enable_master_pooler_load_balancer,omitempty"`
CustomServiceAnnotations map[string]string `json:"custom_service_annotations,omitempty"` EnableReplicaLoadBalancer bool `json:"enable_replica_load_balancer,omitempty"`
MasterDNSNameFormat config.StringTemplate `json:"master_dns_name_format,omitempty"` EnableReplicaPoolerLoadBalancer bool `json:"enable_replica_pooler_load_balancer,omitempty"`
ReplicaDNSNameFormat config.StringTemplate `json:"replica_dns_name_format,omitempty"` CustomServiceAnnotations map[string]string `json:"custom_service_annotations,omitempty"`
ExternalTrafficPolicy string `json:"external_traffic_policy" default:"Cluster"` MasterDNSNameFormat config.StringTemplate `json:"master_dns_name_format,omitempty"`
ReplicaDNSNameFormat config.StringTemplate `json:"replica_dns_name_format,omitempty"`
ExternalTrafficPolicy string `json:"external_traffic_policy" default:"Cluster"`
} }
// AWSGCPConfiguration defines the configuration for AWS // AWSGCPConfiguration defines the configuration for AWS
@@ -213,6 +219,7 @@ type OperatorLogicalBackupConfiguration struct {
S3AccessKeyID string `json:"logical_backup_s3_access_key_id,omitempty"` S3AccessKeyID string `json:"logical_backup_s3_access_key_id,omitempty"`
S3SecretAccessKey string `json:"logical_backup_s3_secret_access_key,omitempty"` S3SecretAccessKey string `json:"logical_backup_s3_secret_access_key,omitempty"`
S3SSE string `json:"logical_backup_s3_sse,omitempty"` S3SSE string `json:"logical_backup_s3_sse,omitempty"`
RetentionTime string `json:"logical_backup_s3_retention_time,omitempty"`
GoogleApplicationCredentials string `json:"logical_backup_google_application_credentials,omitempty"` GoogleApplicationCredentials string `json:"logical_backup_google_application_credentials,omitempty"`
JobPrefix string `json:"logical_backup_job_prefix,omitempty"` JobPrefix string `json:"logical_backup_job_prefix,omitempty"`
} }
+13 -11
View File
@@ -27,7 +27,7 @@ type PostgresSpec struct {
PostgresqlParam `json:"postgresql"` PostgresqlParam `json:"postgresql"`
Volume `json:"volume,omitempty"` Volume `json:"volume,omitempty"`
Patroni `json:"patroni,omitempty"` Patroni `json:"patroni,omitempty"`
Resources `json:"resources,omitempty"` *Resources `json:"resources,omitempty"`
EnableConnectionPooler *bool `json:"enableConnectionPooler,omitempty"` EnableConnectionPooler *bool `json:"enableConnectionPooler,omitempty"`
EnableReplicaConnectionPooler *bool `json:"enableReplicaConnectionPooler,omitempty"` EnableReplicaConnectionPooler *bool `json:"enableReplicaConnectionPooler,omitempty"`
@@ -42,8 +42,10 @@ type PostgresSpec struct {
// vars that enable load balancers are pointers because it is important to know if any of them is omitted from the Postgres manifest // vars that enable load balancers are pointers because it is important to know if any of them is omitted from the Postgres manifest
// in that case the var evaluates to nil and the value is taken from the operator config // in that case the var evaluates to nil and the value is taken from the operator config
EnableMasterLoadBalancer *bool `json:"enableMasterLoadBalancer,omitempty"` EnableMasterLoadBalancer *bool `json:"enableMasterLoadBalancer,omitempty"`
EnableReplicaLoadBalancer *bool `json:"enableReplicaLoadBalancer,omitempty"` EnableMasterPoolerLoadBalancer *bool `json:"enableMasterPoolerLoadBalancer,omitempty"`
EnableReplicaLoadBalancer *bool `json:"enableReplicaLoadBalancer,omitempty"`
EnableReplicaPoolerLoadBalancer *bool `json:"enableReplicaPoolerLoadBalancer,omitempty"`
// deprecated load balancer settings maintained for backward compatibility // deprecated load balancer settings maintained for backward compatibility
// see "Load balancers" operator docs // see "Load balancers" operator docs
@@ -165,7 +167,7 @@ type Patroni struct {
Slots map[string]map[string]string `json:"slots,omitempty"` Slots map[string]map[string]string `json:"slots,omitempty"`
SynchronousMode bool `json:"synchronous_mode,omitempty"` SynchronousMode bool `json:"synchronous_mode,omitempty"`
SynchronousModeStrict bool `json:"synchronous_mode_strict,omitempty"` SynchronousModeStrict bool `json:"synchronous_mode_strict,omitempty"`
SynchronousNodeCount uint32 `json:"synchronous_node_count,omitempty" defaults:1` SynchronousNodeCount uint32 `json:"synchronous_node_count,omitempty" defaults:"1"`
} }
// StandbyDescription contains remote primary config or s3 wal path // StandbyDescription contains remote primary config or s3 wal path
@@ -199,7 +201,7 @@ type CloneDescription struct {
// Sidecar defines a container to be run in the same pod as the Postgres container. // Sidecar defines a container to be run in the same pod as the Postgres container.
type Sidecar struct { type Sidecar struct {
Resources `json:"resources,omitempty"` *Resources `json:"resources,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
DockerImage string `json:"image,omitempty"` DockerImage string `json:"image,omitempty"`
Ports []v1.ContainerPort `json:"ports,omitempty"` Ports []v1.ContainerPort `json:"ports,omitempty"`
@@ -232,19 +234,19 @@ type ConnectionPooler struct {
DockerImage string `json:"dockerImage,omitempty"` DockerImage string `json:"dockerImage,omitempty"`
MaxDBConnections *int32 `json:"maxDBConnections,omitempty"` MaxDBConnections *int32 `json:"maxDBConnections,omitempty"`
Resources `json:"resources,omitempty"` *Resources `json:"resources,omitempty"`
} }
type Stream struct { type Stream struct {
ApplicationId string `json:"applicationId"` ApplicationId string `json:"applicationId"`
Database string `json:"database"` Database string `json:"database"`
Tables map[string]StreamTable `json:"tables"` Tables map[string]StreamTable `json:"tables"`
Filter map[string]string `json:"filter,omitempty"` Filter map[string]*string `json:"filter,omitempty"`
BatchSize uint32 `json:"batchSize,omitempty"` BatchSize *uint32 `json:"batchSize,omitempty"`
} }
type StreamTable struct { type StreamTable struct {
EventType string `json:"eventType"` EventType string `json:"eventType"`
IdColumn string `json:"idColumn,omitempty" defaults:"id"` IdColumn *string `json:"idColumn,omitempty"`
PayloadColumn string `json:"payloadColumn,omitempty" defaults:"payload"` PayloadColumn *string `json:"payloadColumn,omitempty"`
} }
+6 -6
View File
@@ -163,7 +163,7 @@ var unmarshalCluster = []struct {
"kind": "Postgresql","apiVersion": "acid.zalan.do/v1", "kind": "Postgresql","apiVersion": "acid.zalan.do/v1",
"metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": 100}}`), &tmp).Error(), "metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": 100}}`), &tmp).Error(),
}, },
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":"Invalid"}`), marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":"Invalid"}`),
err: nil}, err: nil},
{ {
about: "example with /status subresource", about: "example with /status subresource",
@@ -184,7 +184,7 @@ var unmarshalCluster = []struct {
"kind": "Postgresql","apiVersion": "acid.zalan.do/v1", "kind": "Postgresql","apiVersion": "acid.zalan.do/v1",
"metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": 100}}`), &tmp).Error(), "metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": 100}}`), &tmp).Error(),
}, },
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":{"PostgresClusterStatus":"Invalid"}}`), marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":{"PostgresClusterStatus":"Invalid"}}`),
err: nil}, err: nil},
{ {
about: "example with detailed input manifest and deprecated pod_priority_class_name -> podPriorityClassName", about: "example with detailed input manifest and deprecated pod_priority_class_name -> podPriorityClassName",
@@ -300,7 +300,7 @@ var unmarshalCluster = []struct {
MaximumLagOnFailover: 33554432, MaximumLagOnFailover: 33554432,
Slots: map[string]map[string]string{"permanent_logical_1": {"type": "logical", "database": "foo", "plugin": "pgoutput"}}, Slots: map[string]map[string]string{"permanent_logical_1": {"type": "logical", "database": "foo", "plugin": "pgoutput"}},
}, },
Resources: Resources{ Resources: &Resources{
ResourceRequests: ResourceDescription{CPU: "10m", Memory: "50Mi"}, ResourceRequests: ResourceDescription{CPU: "10m", Memory: "50Mi"},
ResourceLimits: ResourceDescription{CPU: "300m", Memory: "3000Mi"}, ResourceLimits: ResourceDescription{CPU: "300m", Memory: "3000Mi"},
}, },
@@ -351,7 +351,7 @@ var unmarshalCluster = []struct {
Status: PostgresStatus{PostgresClusterStatus: ClusterStatusInvalid}, Status: PostgresStatus{PostgresClusterStatus: ClusterStatusInvalid},
Error: errors.New("name must match {TEAM}-{NAME} format").Error(), Error: errors.New("name must match {TEAM}-{NAME} format").Error(),
}, },
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"teapot-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null} ,"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":{"PostgresClusterStatus":"Invalid"}}`), marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"teapot-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":{"PostgresClusterStatus":"Invalid"}}`),
err: nil}, err: nil},
{ {
about: "example with clone", about: "example with clone",
@@ -373,7 +373,7 @@ var unmarshalCluster = []struct {
}, },
Error: "", Error: "",
}, },
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{"cluster":"team-batman"}},"status":{"PostgresClusterStatus":""}}`), marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{"cluster":"team-batman"}},"status":{"PostgresClusterStatus":""}}`),
err: nil}, err: nil},
{ {
about: "standby example", about: "standby example",
@@ -395,7 +395,7 @@ var unmarshalCluster = []struct {
}, },
Error: "", Error: "",
}, },
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"standby":{"s3_wal_path":"s3://custom/path/to/bucket/"}},"status":{"PostgresClusterStatus":""}}`), marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"standby":{"s3_wal_path":"s3://custom/path/to/bucket/"}},"status":{"PostgresClusterStatus":""}}`),
err: nil}, err: nil},
{ {
about: "expect error on malformatted JSON", about: "expect error on malformatted JSON",
@@ -106,7 +106,11 @@ func (in *ConnectionPooler) DeepCopyInto(out *ConnectionPooler) {
*out = new(int32) *out = new(int32)
**out = **in **out = **in
} }
out.Resources = in.Resources if in.Resources != nil {
in, out := &in.Resources, &out.Resources
*out = new(Resources)
**out = **in
}
return return
} }
@@ -224,6 +228,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.IgnoredAnnotations != nil {
in, out := &in.IgnoredAnnotations, &out.IgnoredAnnotations
*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))
@@ -401,7 +410,7 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData
(*in)[i].DeepCopyInto(&(*out)[i]) (*in)[i].DeepCopyInto(&(*out)[i])
} }
} }
out.PostgresUsersConfiguration = in.PostgresUsersConfiguration in.PostgresUsersConfiguration.DeepCopyInto(&out.PostgresUsersConfiguration)
in.MajorVersionUpgrade.DeepCopyInto(&out.MajorVersionUpgrade) in.MajorVersionUpgrade.DeepCopyInto(&out.MajorVersionUpgrade)
in.Kubernetes.DeepCopyInto(&out.Kubernetes) in.Kubernetes.DeepCopyInto(&out.Kubernetes)
out.PostgresPodResources = in.PostgresPodResources out.PostgresPodResources = in.PostgresPodResources
@@ -575,7 +584,11 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) {
in.PostgresqlParam.DeepCopyInto(&out.PostgresqlParam) in.PostgresqlParam.DeepCopyInto(&out.PostgresqlParam)
in.Volume.DeepCopyInto(&out.Volume) in.Volume.DeepCopyInto(&out.Volume)
in.Patroni.DeepCopyInto(&out.Patroni) in.Patroni.DeepCopyInto(&out.Patroni)
out.Resources = in.Resources if in.Resources != nil {
in, out := &in.Resources, &out.Resources
*out = new(Resources)
**out = **in
}
if in.EnableConnectionPooler != nil { if in.EnableConnectionPooler != nil {
in, out := &in.EnableConnectionPooler, &out.EnableConnectionPooler in, out := &in.EnableConnectionPooler, &out.EnableConnectionPooler
*out = new(bool) *out = new(bool)
@@ -611,11 +624,21 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) {
*out = new(bool) *out = new(bool)
**out = **in **out = **in
} }
if in.EnableMasterPoolerLoadBalancer != nil {
in, out := &in.EnableMasterPoolerLoadBalancer, &out.EnableMasterPoolerLoadBalancer
*out = new(bool)
**out = **in
}
if in.EnableReplicaLoadBalancer != nil { if in.EnableReplicaLoadBalancer != nil {
in, out := &in.EnableReplicaLoadBalancer, &out.EnableReplicaLoadBalancer in, out := &in.EnableReplicaLoadBalancer, &out.EnableReplicaLoadBalancer
*out = new(bool) *out = new(bool)
**out = **in **out = **in
} }
if in.EnableReplicaPoolerLoadBalancer != nil {
in, out := &in.EnableReplicaPoolerLoadBalancer, &out.EnableReplicaPoolerLoadBalancer
*out = new(bool)
**out = **in
}
if in.UseLoadBalancer != nil { if in.UseLoadBalancer != nil {
in, out := &in.UseLoadBalancer, &out.UseLoadBalancer in, out := &in.UseLoadBalancer, &out.UseLoadBalancer
*out = new(bool) *out = new(bool)
@@ -916,6 +939,11 @@ func (in *PostgresTeamSpec) DeepCopy() *PostgresTeamSpec {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgresUsersConfiguration) DeepCopyInto(out *PostgresUsersConfiguration) { func (in *PostgresUsersConfiguration) DeepCopyInto(out *PostgresUsersConfiguration) {
*out = *in *out = *in
if in.AdditionalOwnerRoles != nil {
in, out := &in.AdditionalOwnerRoles, &out.AdditionalOwnerRoles
*out = make([]string, len(*in))
copy(*out, *in)
}
return return
} }
@@ -1117,7 +1145,11 @@ func (in *ScalyrConfiguration) DeepCopy() *ScalyrConfiguration {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Sidecar) DeepCopyInto(out *Sidecar) { func (in *Sidecar) DeepCopyInto(out *Sidecar) {
*out = *in *out = *in
out.Resources = in.Resources if in.Resources != nil {
in, out := &in.Resources, &out.Resources
*out = new(Resources)
**out = **in
}
if in.Ports != nil { if in.Ports != nil {
in, out := &in.Ports, &out.Ports in, out := &in.Ports, &out.Ports
*out = make([]corev1.ContainerPort, len(*in)) *out = make([]corev1.ContainerPort, len(*in))
@@ -1166,16 +1198,29 @@ func (in *Stream) DeepCopyInto(out *Stream) {
in, out := &in.Tables, &out.Tables in, out := &in.Tables, &out.Tables
*out = make(map[string]StreamTable, len(*in)) *out = make(map[string]StreamTable, len(*in))
for key, val := range *in { for key, val := range *in {
(*out)[key] = val (*out)[key] = *val.DeepCopy()
} }
} }
if in.Filter != nil { if in.Filter != nil {
in, out := &in.Filter, &out.Filter in, out := &in.Filter, &out.Filter
*out = make(map[string]string, len(*in)) *out = make(map[string]*string, len(*in))
for key, val := range *in { for key, val := range *in {
(*out)[key] = val var outVal *string
if val == nil {
(*out)[key] = nil
} else {
in, out := &val, &outVal
*out = new(string)
**out = **in
}
(*out)[key] = outVal
} }
} }
if in.BatchSize != nil {
in, out := &in.BatchSize, &out.BatchSize
*out = new(uint32)
**out = **in
}
return return
} }
@@ -1192,6 +1237,16 @@ func (in *Stream) DeepCopy() *Stream {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StreamTable) DeepCopyInto(out *StreamTable) { func (in *StreamTable) DeepCopyInto(out *StreamTable) {
*out = *in *out = *in
if in.IdColumn != nil {
in, out := &in.IdColumn, &out.IdColumn
*out = new(string)
**out = **in
}
if in.PayloadColumn != nil {
in, out := &in.PayloadColumn, &out.PayloadColumn
*out = new(string)
**out = **in
}
return return
} }
+13 -13
View File
@@ -40,15 +40,15 @@ type EventStream struct {
// EventStreamFlow defines the flow characteristics of the event stream // EventStreamFlow defines the flow characteristics of the event stream
type EventStreamFlow struct { type EventStreamFlow struct {
Type string `json:"type"` Type string `json:"type"`
PayloadColumn string `json:"payloadColumn,omitempty" defaults:"payload"` PayloadColumn *string `json:"payloadColumn,omitempty"`
} }
// EventStreamSink defines the target of the event stream // EventStreamSink defines the target of the event stream
type EventStreamSink struct { type EventStreamSink struct {
Type string `json:"type"` Type string `json:"type"`
EventType string `json:"eventType,omitempty"` EventType string `json:"eventType,omitempty"`
MaxBatchSize uint32 `json:"maxBatchSize,omitempty"` MaxBatchSize *uint32 `json:"maxBatchSize,omitempty"`
} }
// EventStreamSource defines the source of the event stream and connection for FES operator // EventStreamSource defines the source of the event stream and connection for FES operator
@@ -56,23 +56,23 @@ type EventStreamSource struct {
Type string `json:"type"` Type string `json:"type"`
Schema string `json:"schema,omitempty" defaults:"public"` Schema string `json:"schema,omitempty" defaults:"public"`
EventStreamTable EventStreamTable `json:"table"` EventStreamTable EventStreamTable `json:"table"`
Filter string `json:"filter,omitempty"` Filter *string `json:"filter,omitempty"`
Connection Connection `json:"jdbcConnection"` Connection Connection `json:"jdbcConnection"`
} }
// EventStreamTable defines the name and ID column to be used for streaming // EventStreamTable defines the name and ID column to be used for streaming
type EventStreamTable struct { type EventStreamTable struct {
Name string `json:"name"` Name string `json:"name"`
IDColumn string `json:"idColumn,omitempty" defaults:"id"` IDColumn *string `json:"idColumn,omitempty"`
} }
// Connection to be used for allowing the FES operator to connect to a database // Connection to be used for allowing the FES operator to connect to a database
type Connection struct { type Connection struct {
Url string `json:"jdbcUrl"` Url string `json:"jdbcUrl"`
SlotName string `json:"slotName"` SlotName string `json:"slotName"`
PluginType string `json:"pluginType,omitempty" defaults:"wal2json"` PluginType string `json:"pluginType,omitempty"`
PublicationName string `json:"publicationName,omitempty"` PublicationName *string `json:"publicationName,omitempty"`
DBAuth DBAuth `json:"databaseAuthentication"` DBAuth DBAuth `json:"databaseAuthentication"`
} }
// DBAuth specifies the credentials to be used for connecting with the database // DBAuth specifies the credentials to be used for connecting with the database
+96 -60
View File
@@ -43,7 +43,7 @@ var (
alphaNumericRegexp = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9]*$") alphaNumericRegexp = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9]*$")
databaseNameRegexp = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$") databaseNameRegexp = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$")
userRegexp = regexp.MustCompile(`^[a-z0-9]([-_a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-_a-z0-9]*[a-z0-9])?)*$`) userRegexp = regexp.MustCompile(`^[a-z0-9]([-_a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-_a-z0-9]*[a-z0-9])?)*$`)
patroniObjectSuffixes = []string{"config", "failover", "sync"} patroniObjectSuffixes = []string{"config", "failover", "sync", "leader"}
) )
// Config contains operator-wide clients and configuration used from a cluster. TODO: remove struct duplication. // Config contains operator-wide clients and configuration used from a cluster. TODO: remove struct duplication.
@@ -228,6 +228,8 @@ func (c *Cluster) initUsers() error {
return fmt.Errorf("could not init human users: %v", err) return fmt.Errorf("could not init human users: %v", err)
} }
c.initAdditionalOwnerRoles()
return nil return nil
} }
@@ -254,24 +256,22 @@ func (c *Cluster) Create() error {
c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusCreating) c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusCreating)
c.eventRecorder.Event(c.GetReference(), v1.EventTypeNormal, "Create", "Started creation of new cluster resources") c.eventRecorder.Event(c.GetReference(), v1.EventTypeNormal, "Create", "Started creation of new cluster resources")
if err = c.enforceMinResourceLimits(&c.Spec); err != nil {
return fmt.Errorf("could not enforce minimum resource limits: %v", err)
}
for _, role := range []PostgresRole{Master, Replica} { for _, role := range []PostgresRole{Master, Replica} {
if c.Endpoints[role] != nil { if !c.patroniKubernetesUseConfigMaps() {
return fmt.Errorf("%s endpoint already exists in the cluster", role) if c.Endpoints[role] != nil {
} return fmt.Errorf("%s endpoint already exists in the cluster", role)
if role == Master { }
// replica endpoint will be created by the replica service. Master endpoint needs to be created by us, if role == Master {
// since the corresponding master service does not define any selectors. // replica endpoint will be created by the replica service. Master endpoint needs to be created by us,
ep, err = c.createEndpoint(role) // since the corresponding master service does not define any selectors.
if err != nil { ep, err = c.createEndpoint(role)
return fmt.Errorf("could not create %s endpoint: %v", role, err) if err != nil {
return fmt.Errorf("could not create %s endpoint: %v", role, err)
}
c.logger.Infof("endpoint %q has been successfully created", util.NameFromMeta(ep.ObjectMeta))
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeNormal, "Endpoints", "Endpoint %q has been successfully created", util.NameFromMeta(ep.ObjectMeta))
} }
c.logger.Infof("endpoint %q has been successfully created", util.NameFromMeta(ep.ObjectMeta))
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeNormal, "Endpoints", "Endpoint %q has been successfully created", util.NameFromMeta(ep.ObjectMeta))
} }
if c.Services[role] != nil { if c.Services[role] != nil {
@@ -380,9 +380,10 @@ func (c *Cluster) compareStatefulSetWith(statefulSet *appsv1.StatefulSet) *compa
match = false match = false
reasons = append(reasons, "new statefulset's number of replicas does not match the current one") reasons = append(reasons, "new statefulset's number of replicas does not match the current one")
} }
if !reflect.DeepEqual(c.Statefulset.Annotations, statefulSet.Annotations) { if changed, reason := c.compareAnnotations(c.Statefulset.Annotations, statefulSet.Annotations); changed {
match = false
needsReplace = true needsReplace = true
reasons = append(reasons, "new statefulset's annotations do not match the current one") reasons = append(reasons, "new statefulset's annotations do not match: "+reason)
} }
needsRollUpdate, reasons = c.compareContainers("initContainers", c.Statefulset.Spec.Template.Spec.InitContainers, statefulSet.Spec.Template.Spec.InitContainers, needsRollUpdate, reasons) needsRollUpdate, reasons = c.compareContainers("initContainers", c.Statefulset.Spec.Template.Spec.InitContainers, statefulSet.Spec.Template.Spec.InitContainers, needsRollUpdate, reasons)
@@ -436,10 +437,11 @@ func (c *Cluster) compareStatefulSetWith(statefulSet *appsv1.StatefulSet) *compa
} }
} }
if !reflect.DeepEqual(c.Statefulset.Spec.Template.Annotations, statefulSet.Spec.Template.Annotations) { if changed, reason := c.compareAnnotations(c.Statefulset.Spec.Template.Annotations, statefulSet.Spec.Template.Annotations); changed {
match = false
needsReplace = true needsReplace = true
needsRollUpdate = true needsRollUpdate = true
reasons = append(reasons, "new statefulset's pod template metadata annotations does not match the current one") reasons = append(reasons, "new statefulset's pod template metadata annotations does not match "+reason)
} }
if !reflect.DeepEqual(c.Statefulset.Spec.Template.Spec.SecurityContext, statefulSet.Spec.Template.Spec.SecurityContext) { if !reflect.DeepEqual(c.Statefulset.Spec.Template.Spec.SecurityContext, statefulSet.Spec.Template.Spec.SecurityContext) {
needsReplace = true needsReplace = true
@@ -674,44 +676,59 @@ func comparePorts(a, b []v1.ContainerPort) bool {
return true return true
} }
func (c *Cluster) enforceMinResourceLimits(spec *acidv1.PostgresSpec) error { func (c *Cluster) compareAnnotations(old, new map[string]string) (bool, string) {
reason := ""
ignoredAnnotations := make(map[string]bool)
for _, ignore := range c.OpConfig.IgnoredAnnotations {
ignoredAnnotations[ignore] = true
}
var ( for key := range old {
isSmaller bool if _, ok := ignoredAnnotations[key]; ok {
err error continue
)
// setting limits too low can cause unnecessary evictions / OOM kills
minCPULimit := c.OpConfig.MinCPULimit
minMemoryLimit := c.OpConfig.MinMemoryLimit
cpuLimit := spec.Resources.ResourceLimits.CPU
if cpuLimit != "" {
isSmaller, err = util.IsSmallerQuantity(cpuLimit, minCPULimit)
if err != nil {
return fmt.Errorf("could not compare defined CPU limit %s with configured minimum value %s: %v", cpuLimit, minCPULimit, err)
} }
if isSmaller { if _, ok := new[key]; !ok {
c.logger.Warningf("defined CPU limit %s is below required minimum %s and will be increased", cpuLimit, minCPULimit) reason += fmt.Sprintf(" Removed %q.", key)
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "ResourceLimits", "defined CPU limit %s is below required minimum %s and will be set to it", cpuLimit, minCPULimit)
spec.Resources.ResourceLimits.CPU = minCPULimit
} }
} }
memoryLimit := spec.Resources.ResourceLimits.Memory for key := range new {
if memoryLimit != "" { if _, ok := ignoredAnnotations[key]; ok {
isSmaller, err = util.IsSmallerQuantity(memoryLimit, minMemoryLimit) continue
if err != nil {
return fmt.Errorf("could not compare defined memory limit %s with configured minimum value %s: %v", memoryLimit, minMemoryLimit, err)
} }
if isSmaller { v, ok := old[key]
c.logger.Warningf("defined memory limit %s is below required minimum %s and will be increased", memoryLimit, minMemoryLimit) if !ok {
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "ResourceLimits", "defined memory limit %s is below required minimum %s and will be set to it", memoryLimit, minMemoryLimit) reason += fmt.Sprintf(" Added %q with value %q.", key, new[key])
spec.Resources.ResourceLimits.Memory = minMemoryLimit } else if v != new[key] {
reason += fmt.Sprintf(" %q changed from %q to %q.", key, v, new[key])
} }
} }
return nil return reason != "", reason
}
func (c *Cluster) compareServices(old, new *v1.Service) (bool, string) {
if old.Spec.Type != new.Spec.Type {
return false, fmt.Sprintf("new service's type %q does not match the current one %q",
new.Spec.Type, old.Spec.Type)
}
oldSourceRanges := old.Spec.LoadBalancerSourceRanges
newSourceRanges := new.Spec.LoadBalancerSourceRanges
/* work around Kubernetes 1.6 serializing [] as nil. See https://github.com/kubernetes/kubernetes/issues/43203 */
if (len(oldSourceRanges) != 0) || (len(newSourceRanges) != 0) {
if !util.IsEqualIgnoreOrder(oldSourceRanges, newSourceRanges) {
return false, "new service's LoadBalancerSourceRange does not match the current one"
}
}
if changed, reason := c.compareAnnotations(old.Annotations, new.Annotations); changed {
return !changed, "new service's annotations does not match the current one:" + reason
}
return true, ""
} }
// Update changes Kubernetes objects according to the new specification. Unlike the sync case, the missing object // Update changes Kubernetes objects according to the new specification. Unlike the sync case, the missing object
@@ -793,12 +810,6 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
// Statefulset // Statefulset
func() { func() {
if err := c.enforceMinResourceLimits(&c.Spec); err != nil {
c.logger.Errorf("could not sync resources: %v", err)
updateFailed = true
return
}
oldSs, err := c.generateStatefulSet(&oldSpec.Spec) oldSs, err := c.generateStatefulSet(&oldSpec.Spec)
if err != nil { if err != nil {
c.logger.Errorf("could not generate old statefulset spec: %v", err) c.logger.Errorf("could not generate old statefulset spec: %v", err)
@@ -806,16 +817,13 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
return return
} }
// update newSpec to for latter comparison with oldSpec
c.enforceMinResourceLimits(&newSpec.Spec)
newSs, err := c.generateStatefulSet(&newSpec.Spec) newSs, err := c.generateStatefulSet(&newSpec.Spec)
if err != nil { if err != nil {
c.logger.Errorf("could not generate new statefulset spec: %v", err) c.logger.Errorf("could not generate new statefulset spec: %v", err)
updateFailed = true updateFailed = true
return return
} }
if syncStatefulSet || !reflect.DeepEqual(oldSs, newSs) || !reflect.DeepEqual(oldSpec.Annotations, newSpec.Annotations) { if syncStatefulSet || !reflect.DeepEqual(oldSs, newSs) {
c.logger.Debugf("syncing statefulsets") c.logger.Debugf("syncing statefulsets")
syncStatefulSet = false syncStatefulSet = false
// TODO: avoid generating the StatefulSet object twice by passing it to syncStatefulSet // TODO: avoid generating the StatefulSet object twice by passing it to syncStatefulSet
@@ -1297,6 +1305,33 @@ func (c *Cluster) initRobotUsers() error {
return nil return nil
} }
func (c *Cluster) initAdditionalOwnerRoles() {
for _, additionalOwner := range c.OpConfig.AdditionalOwnerRoles {
// fetch all database owners the additional should become a member of
memberOf := make([]string, 0)
for username, pgUser := range c.pgUsers {
if pgUser.IsDbOwner {
memberOf = append(memberOf, username)
}
}
if len(memberOf) > 1 {
namespace := c.Namespace
additionalOwnerPgUser := spec.PgUser{
Origin: spec.RoleOriginSpilo,
MemberOf: memberOf,
Name: additionalOwner,
Namespace: namespace,
}
if currentRole, present := c.pgUsers[additionalOwner]; present {
c.pgUsers[additionalOwner] = c.resolveNameConflict(&currentRole, &additionalOwnerPgUser)
} else {
c.pgUsers[additionalOwner] = additionalOwnerPgUser
}
}
}
}
func (c *Cluster) initTeamMembers(teamID string, isPostgresSuperuserTeam bool) error { func (c *Cluster) initTeamMembers(teamID string, isPostgresSuperuserTeam bool) error {
teamMembers, err := c.getTeamMembers(teamID) teamMembers, err := c.getTeamMembers(teamID)
@@ -1543,8 +1578,9 @@ func (c *Cluster) deletePatroniClusterObjects() error {
if !c.patroniKubernetesUseConfigMaps() { if !c.patroniKubernetesUseConfigMaps() {
actionsList = append(actionsList, c.deletePatroniClusterEndpoints) actionsList = append(actionsList, c.deletePatroniClusterEndpoints)
} else {
actionsList = append(actionsList, c.deletePatroniClusterServices, c.deletePatroniClusterConfigMaps)
} }
actionsList = append(actionsList, c.deletePatroniClusterServices, c.deletePatroniClusterConfigMaps)
c.logger.Debugf("removing leftover Patroni objects (endpoints / services and configmaps)") c.logger.Debugf("removing leftover Patroni objects (endpoints / services and configmaps)")
for _, deleter := range actionsList { for _, deleter := range actionsList {
+387 -6
View File
@@ -3,6 +3,7 @@ package cluster
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -11,6 +12,7 @@ import (
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
fakeacidv1 "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/fake" fakeacidv1 "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/fake"
"github.com/zalando/postgres-operator/pkg/spec" "github.com/zalando/postgres-operator/pkg/spec"
"github.com/zalando/postgres-operator/pkg/util"
"github.com/zalando/postgres-operator/pkg/util/config" "github.com/zalando/postgres-operator/pkg/util/config"
"github.com/zalando/postgres-operator/pkg/util/constants" "github.com/zalando/postgres-operator/pkg/util/constants"
"github.com/zalando/postgres-operator/pkg/util/k8sutil" "github.com/zalando/postgres-operator/pkg/util/k8sutil"
@@ -33,10 +35,11 @@ var cl = New(
Config{ Config{
OpConfig: config.Config{ OpConfig: config.Config{
PodManagementPolicy: "ordered_ready", PodManagementPolicy: "ordered_ready",
ProtectedRoles: []string{"admin"}, ProtectedRoles: []string{"admin", "cron_admin", "part_man"},
Auth: config.Auth{ Auth: config.Auth{
SuperUsername: superUserName, SuperUsername: superUserName,
ReplicationUsername: replicationUserName, ReplicationUsername: replicationUserName,
AdditionalOwnerRoles: []string{"cron_admin", "part_man"},
}, },
Resources: config.Resources{ Resources: config.Resources{
DownscalerAnnotations: []string{"downscaler/*"}, DownscalerAnnotations: []string{"downscaler/*"},
@@ -44,7 +47,13 @@ var cl = New(
}, },
}, },
k8sutil.NewMockKubernetesClient(), k8sutil.NewMockKubernetesClient(),
acidv1.Postgresql{ObjectMeta: metav1.ObjectMeta{Name: "acid-test", Namespace: "test", Annotations: map[string]string{"downscaler/downtime_replicas": "0"}}}, acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: "acid-test",
Namespace: "test",
Annotations: map[string]string{"downscaler/downtime_replicas": "0"},
},
},
logger, logger,
eventRecorder, eventRecorder,
) )
@@ -53,7 +62,7 @@ func TestStatefulSetAnnotations(t *testing.T) {
testName := "CheckStatefulsetAnnotations" testName := "CheckStatefulsetAnnotations"
spec := acidv1.PostgresSpec{ spec := acidv1.PostgresSpec{
TeamID: "myapp", NumberOfInstances: 1, TeamID: "myapp", NumberOfInstances: 1,
Resources: acidv1.Resources{ Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"}, ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"}, ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
}, },
@@ -132,6 +141,48 @@ func TestInitRobotUsers(t *testing.T) {
} }
} }
func TestInitAdditionalOwnerRoles(t *testing.T) {
testName := "TestInitAdditionalOwnerRoles"
manifestUsers := map[string]acidv1.UserFlags{"foo_owner": {}, "bar_owner": {}, "app_user": {}}
expectedUsers := map[string]spec.PgUser{
"foo_owner": {Origin: spec.RoleOriginManifest, Name: "foo_owner", Namespace: cl.Namespace, Password: "f123", Flags: []string{"LOGIN"}, IsDbOwner: true},
"bar_owner": {Origin: spec.RoleOriginManifest, Name: "bar_owner", Namespace: cl.Namespace, Password: "b123", Flags: []string{"LOGIN"}, IsDbOwner: true},
"app_user": {Origin: spec.RoleOriginManifest, Name: "app_user", Namespace: cl.Namespace, Password: "a123", Flags: []string{"LOGIN"}, IsDbOwner: false},
"cron_admin": {Origin: spec.RoleOriginSpilo, Name: "cron_admin", Namespace: cl.Namespace, MemberOf: []string{"foo_owner", "bar_owner"}},
"part_man": {Origin: spec.RoleOriginSpilo, Name: "part_man", Namespace: cl.Namespace, MemberOf: []string{"foo_owner", "bar_owner"}},
}
cl.Spec.Databases = map[string]string{"foo_db": "foo_owner", "bar_db": "bar_owner"}
cl.Spec.Users = manifestUsers
// this should set IsDbOwner field for manifest users
if err := cl.initRobotUsers(); err != nil {
t.Errorf("%s could not init manifest users", testName)
}
// update passwords to compare with result
for manifestUser := range manifestUsers {
pgUser := cl.pgUsers[manifestUser]
pgUser.Password = manifestUser[0:1] + "123"
cl.pgUsers[manifestUser] = pgUser
}
cl.initAdditionalOwnerRoles()
for _, additionalOwnerRole := range cl.Config.OpConfig.AdditionalOwnerRoles {
expectedPgUser := expectedUsers[additionalOwnerRole]
existingPgUser, exists := cl.pgUsers[additionalOwnerRole]
if !exists {
t.Errorf("%s additional owner role %q not initilaized", testName, additionalOwnerRole)
}
if !util.IsEqualIgnoreOrder(expectedPgUser.MemberOf, existingPgUser.MemberOf) {
t.Errorf("%s unexpected membership of additional owner role %q: expected member of %#v, got member of %#v",
testName, additionalOwnerRole, expectedPgUser.MemberOf, existingPgUser.MemberOf)
}
}
}
type mockOAuthTokenGetter struct { type mockOAuthTokenGetter struct {
} }
@@ -1004,6 +1055,336 @@ func TestCompareEnv(t *testing.T) {
} }
} }
func newService(ann map[string]string, svcT v1.ServiceType, lbSr []string) *v1.Service {
svc := &v1.Service{
Spec: v1.ServiceSpec{
Type: svcT,
LoadBalancerSourceRanges: lbSr,
},
}
svc.Annotations = ann
return svc
}
func TestCompareServices(t *testing.T) {
testName := "TestCompareServices"
cluster := Cluster{
Config: Config{
OpConfig: config.Config{
Resources: config.Resources{
IgnoredAnnotations: []string{
"k8s.v1.cni.cncf.io/network-status",
},
},
},
},
}
tests := []struct {
about string
current *v1.Service
new *v1.Service
reason string
match bool
}{
{
about: "two equal services",
current: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeClusterIP,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeClusterIP,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: true,
},
{
about: "services differ on service type",
current: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeClusterIP,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
reason: `new service's type "LoadBalancer" does not match the current one "ClusterIP"`,
},
{
about: "services differ on lb source ranges",
current: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"185.249.56.0/22"}),
match: false,
reason: `new service's LoadBalancerSourceRange does not match the current one`,
},
{
about: "new service doesn't have lb source ranges",
current: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{}),
match: false,
reason: `new service's LoadBalancerSourceRange does not match the current one`,
},
{
about: "services differ on DNS annotation",
current: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "new_clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
reason: `new service's annotations does not match the current one: "external-dns.alpha.kubernetes.io/hostname" changed from "clstr.acid.zalan.do" to "new_clstr.acid.zalan.do".`,
},
{
about: "services differ on AWS ELB annotation",
current: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: "1800",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
reason: `new service's annotations does not match the current one: "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout" changed from "3600" to "1800".`,
},
{
about: "service changes existing annotation",
current: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"foo": "bar",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"foo": "baz",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
reason: `new service's annotations does not match the current one: "foo" changed from "bar" to "baz".`,
},
{
about: "service changes multiple existing annotations",
current: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"foo": "bar",
"bar": "foo",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"foo": "baz",
"bar": "fooz",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
// Test just the prefix to avoid flakiness and map sorting
reason: `new service's annotations does not match the current one:`,
},
{
about: "service adds a new custom annotation",
current: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"foo": "bar",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
reason: `new service's annotations does not match the current one: Added "foo" with value "bar".`,
},
{
about: "service removes a custom annotation",
current: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"foo": "bar",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
reason: `new service's annotations does not match the current one: Removed "foo".`,
},
{
about: "service removes a custom annotation and adds a new one",
current: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"foo": "bar",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"bar": "foo",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
reason: `new service's annotations does not match the current one: Removed "foo". Added "bar" with value "foo".`,
},
{
about: "service removes a custom annotation, adds a new one and change another",
current: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"foo": "bar",
"zalan": "do",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"bar": "foo",
"zalan": "do.com",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
// Test just the prefix to avoid flakiness and map sorting
reason: `new service's annotations does not match the current one: Removed "foo".`,
},
{
about: "service add annotations",
current: newService(
map[string]string{},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
// Test just the prefix to avoid flakiness and map sorting
reason: `new service's annotations does not match the current one: Added `,
},
{
about: "ignored annotations",
current: newService(
map[string]string{},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newService(
map[string]string{
"k8s.v1.cni.cncf.io/network-status": "up",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: true,
},
}
for _, tt := range tests {
t.Run(tt.about, func(t *testing.T) {
match, reason := cluster.compareServices(tt.current, tt.new)
if match && !tt.match {
t.Logf("match=%v current=%v, old=%v reason=%s", match, tt.current.Annotations, tt.new.Annotations, reason)
t.Errorf("%s - expected services to do not match: %q and %q", testName, tt.current, tt.new)
return
}
if !match && tt.match {
t.Errorf("%s - expected services to be the same: %q and %q", testName, tt.current, tt.new)
return
}
if !match && !tt.match {
if !strings.HasPrefix(reason, tt.reason) {
t.Errorf("%s - expected reason prefix %s, found %s", testName, tt.reason, reason)
return
}
}
})
}
}
func TestCrossNamespacedSecrets(t *testing.T) { func TestCrossNamespacedSecrets(t *testing.T) {
testName := "test secrets in different namespace" testName := "test secrets in different namespace"
clientSet := fake.NewSimpleClientset() clientSet := fake.NewSimpleClientset()
@@ -1041,7 +1422,7 @@ func TestCrossNamespacedSecrets(t *testing.T) {
ConnectionPoolerDefaultCPULimit: "100m", ConnectionPoolerDefaultCPULimit: "100m",
ConnectionPoolerDefaultMemoryRequest: "100Mi", ConnectionPoolerDefaultMemoryRequest: "100Mi",
ConnectionPoolerDefaultMemoryLimit: "100Mi", ConnectionPoolerDefaultMemoryLimit: "100Mi",
NumberOfInstances: int32ToPointer(1), NumberOfInstances: k8sutil.Int32ToPointer(1),
}, },
PodManagementPolicy: "ordered_ready", PodManagementPolicy: "ordered_ready",
Resources: config.Resources{ Resources: config.Resources{
+96 -78
View File
@@ -3,7 +3,6 @@ package cluster
import ( import (
"context" "context"
"fmt" "fmt"
"reflect"
"strings" "strings"
"github.com/r3labs/diff" "github.com/r3labs/diff"
@@ -211,9 +210,10 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) (
connectionPoolerSpec = &acidv1.ConnectionPooler{} connectionPoolerSpec = &acidv1.ConnectionPooler{}
} }
gracePeriod := int64(c.OpConfig.PodTerminateGracePeriod.Seconds()) gracePeriod := int64(c.OpConfig.PodTerminateGracePeriod.Seconds())
resources, err := generateResourceRequirements( resources, err := c.generateResourceRequirements(
connectionPoolerSpec.Resources, connectionPoolerSpec.Resources,
makeDefaultConnectionPoolerResources(&c.OpConfig)) makeDefaultConnectionPoolerResources(&c.OpConfig),
connectionPoolerContainer)
effectiveDockerImage := util.Coalesce( effectiveDockerImage := util.Coalesce(
connectionPoolerSpec.DockerImage, connectionPoolerSpec.DockerImage,
@@ -247,7 +247,7 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) (
}, },
{ {
Name: "PGPORT", Name: "PGPORT",
Value: c.servicePort(role), Value: fmt.Sprint(c.servicePort(role)),
}, },
{ {
Name: "PGUSER", Name: "PGUSER",
@@ -344,7 +344,7 @@ func (c *Cluster) generateConnectionPoolerDeployment(connectionPooler *Connectio
} }
if *numberOfInstances < constants.ConnectionPoolerMinInstances { if *numberOfInstances < constants.ConnectionPoolerMinInstances {
msg := "Adjusted number of connection pooler instances from %d to %d" msg := "adjusted number of connection pooler instances from %d to %d"
c.logger.Warningf(msg, *numberOfInstances, constants.ConnectionPoolerMinInstances) c.logger.Warningf(msg, *numberOfInstances, constants.ConnectionPoolerMinInstances)
*numberOfInstances = constants.ConnectionPoolerMinInstances *numberOfInstances = constants.ConnectionPoolerMinInstances
@@ -386,7 +386,7 @@ func (c *Cluster) generateConnectionPoolerService(connectionPooler *ConnectionPo
{ {
Name: connectionPooler.Name, Name: connectionPooler.Name,
Port: pgPort, Port: pgPort,
TargetPort: intstr.IntOrString{StrVal: c.servicePort(connectionPooler.Role)}, TargetPort: intstr.IntOrString{IntVal: c.servicePort(connectionPooler.Role)},
}, },
}, },
Type: v1.ServiceTypeClusterIP, Type: v1.ServiceTypeClusterIP,
@@ -395,6 +395,10 @@ func (c *Cluster) generateConnectionPoolerService(connectionPooler *ConnectionPo
}, },
} }
if c.shouldCreateLoadBalancerForPoolerService(connectionPooler.Role, spec) {
c.configureLoadBalanceService(&serviceSpec, spec.AllowedSourceRanges)
}
service := &v1.Service{ service := &v1.Service{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: connectionPooler.Name, Name: connectionPooler.Name,
@@ -415,6 +419,29 @@ func (c *Cluster) generateConnectionPoolerService(connectionPooler *ConnectionPo
return service return service
} }
func (c *Cluster) shouldCreateLoadBalancerForPoolerService(role PostgresRole, spec *acidv1.PostgresSpec) bool {
switch role {
case Replica:
// if the value is explicitly set in a Postgresql manifest, follow this setting
if spec.EnableReplicaPoolerLoadBalancer != nil {
return *spec.EnableReplicaPoolerLoadBalancer
}
// otherwise, follow the operator configuration
return c.OpConfig.EnableReplicaPoolerLoadBalancer
case Master:
if spec.EnableMasterPoolerLoadBalancer != nil {
return *spec.EnableMasterPoolerLoadBalancer
}
return c.OpConfig.EnableMasterPoolerLoadBalancer
default:
panic(fmt.Sprintf("Unknown role %v", role))
}
}
//delete connection pooler //delete connection pooler
func (c *Cluster) deleteConnectionPooler(role PostgresRole) (err error) { func (c *Cluster) deleteConnectionPooler(role PostgresRole) (err error) {
c.logger.Infof("deleting connection pooler spilo-role=%s", role) c.logger.Infof("deleting connection pooler spilo-role=%s", role)
@@ -587,7 +614,7 @@ func (c *Cluster) needSyncConnectionPoolerDefaults(Config *Config, spec *acidv1.
*deployment.Spec.Replicas != *config.NumberOfInstances { *deployment.Spec.Replicas != *config.NumberOfInstances {
sync = true sync = true
msg := fmt.Sprintf("NumberOfInstances is different (having %d, required %d)", msg := fmt.Sprintf("numberOfInstances is different (having %d, required %d)",
*deployment.Spec.Replicas, *config.NumberOfInstances) *deployment.Spec.Replicas, *config.NumberOfInstances)
reasons = append(reasons, msg) reasons = append(reasons, msg)
} }
@@ -596,13 +623,14 @@ func (c *Cluster) needSyncConnectionPoolerDefaults(Config *Config, spec *acidv1.
poolerContainer.Image != config.Image { poolerContainer.Image != config.Image {
sync = true sync = true
msg := fmt.Sprintf("DockerImage is different (having %s, required %s)", msg := fmt.Sprintf("dockerImage is different (having %s, required %s)",
poolerContainer.Image, config.Image) poolerContainer.Image, config.Image)
reasons = append(reasons, msg) reasons = append(reasons, msg)
} }
expectedResources, err := generateResourceRequirements(spec.Resources, expectedResources, err := c.generateResourceRequirements(spec.Resources,
makeDefaultConnectionPoolerResources(&Config.OpConfig)) makeDefaultConnectionPoolerResources(&Config.OpConfig),
connectionPoolerContainer)
// An error to generate expected resources means something is not quite // An error to generate expected resources means something is not quite
// right, but for the purpose of robustness do not panic here, just report // right, but for the purpose of robustness do not panic here, just report
@@ -610,7 +638,7 @@ func (c *Cluster) needSyncConnectionPoolerDefaults(Config *Config, spec *acidv1.
// updates for new resource values). // updates for new resource values).
if err == nil && syncResources(&poolerContainer.Resources, expectedResources) { if err == nil && syncResources(&poolerContainer.Resources, expectedResources) {
sync = true sync = true
msg := fmt.Sprintf("Resources are different (having %+v, required %+v)", msg := fmt.Sprintf("resources are different (having %+v, required %+v)",
poolerContainer.Resources, expectedResources) poolerContainer.Resources, expectedResources)
reasons = append(reasons, msg) reasons = append(reasons, msg)
} }
@@ -695,29 +723,6 @@ func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, Look
var err error var err error
var connectionPoolerNeeded bool var connectionPoolerNeeded bool
needSync := !reflect.DeepEqual(oldSpec.Spec.ConnectionPooler, newSpec.Spec.ConnectionPooler)
masterChanges, err := diff.Diff(oldSpec.Spec.EnableConnectionPooler, newSpec.Spec.EnableConnectionPooler)
if err != nil {
c.logger.Error("Error in getting diff of master connection pooler changes")
}
replicaChanges, err := diff.Diff(oldSpec.Spec.EnableReplicaConnectionPooler, newSpec.Spec.EnableReplicaConnectionPooler)
if err != nil {
c.logger.Error("Error in getting diff of replica connection pooler changes")
}
// skip pooler sync when theres no diff or it's deactivated
// but, handling the case when connectionPooler is not there but it is required
// as per spec, hence do not skip syncing in that case, even though there
// is no diff in specs
if (!needSync && len(masterChanges) <= 0 && len(replicaChanges) <= 0) &&
((!needConnectionPooler(&newSpec.Spec) && (c.ConnectionPooler == nil || !needConnectionPooler(&oldSpec.Spec))) ||
(c.ConnectionPooler != nil && needConnectionPooler(&newSpec.Spec) &&
((c.ConnectionPooler[Master] != nil && c.ConnectionPooler[Master].LookupFunction) ||
(c.ConnectionPooler[Replica] != nil && c.ConnectionPooler[Replica].LookupFunction)))) {
c.logger.Debugln("syncing pooler is not required")
return nil, nil
}
logPoolerEssentials(c.logger, oldSpec, newSpec) logPoolerEssentials(c.logger, oldSpec, newSpec)
// Check and perform the sync requirements for each of the roles. // Check and perform the sync requirements for each of the roles.
@@ -754,7 +759,8 @@ func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, Look
// in between // in between
// in this case also do not forget to install lookup function // in this case also do not forget to install lookup function
if !c.ConnectionPooler[role].LookupFunction { // skip installation in standby clusters, since they are read-only
if !c.ConnectionPooler[role].LookupFunction && c.Spec.StandbyCluster == nil {
connectionPooler := c.Spec.ConnectionPooler connectionPooler := c.Spec.ConnectionPooler
specSchema := "" specSchema := ""
specUser := "" specUser := ""
@@ -811,31 +817,37 @@ func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, Look
func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql, role PostgresRole) ( func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql, role PostgresRole) (
SyncReason, error) { SyncReason, error) {
deployment, err := c.KubeClient. var (
deployment *appsv1.Deployment
newDeployment *appsv1.Deployment
service *v1.Service
newService *v1.Service
err error
)
syncReason := make([]string, 0)
deployment, err = c.KubeClient.
Deployments(c.Namespace). Deployments(c.Namespace).
Get(context.TODO(), c.connectionPoolerName(role), metav1.GetOptions{}) Get(context.TODO(), c.connectionPoolerName(role), metav1.GetOptions{})
if err != nil && k8sutil.ResourceNotFound(err) { if err != nil && k8sutil.ResourceNotFound(err) {
msg := "deployment %s for connection pooler synchronization is not found, create it" c.logger.Warningf("deployment %s for connection pooler synchronization is not found, create it", c.connectionPoolerName(role))
c.logger.Warningf(msg, c.connectionPoolerName(role))
deploymentSpec, err := c.generateConnectionPoolerDeployment(c.ConnectionPooler[role]) newDeployment, err = c.generateConnectionPoolerDeployment(c.ConnectionPooler[role])
if err != nil { if err != nil {
msg = "could not generate deployment for connection pooler: %v" return NoSync, fmt.Errorf("could not generate deployment for connection pooler: %v", err)
return NoSync, fmt.Errorf(msg, err)
} }
deployment, err := c.KubeClient. deployment, err = c.KubeClient.
Deployments(deploymentSpec.Namespace). Deployments(newDeployment.Namespace).
Create(context.TODO(), deploymentSpec, metav1.CreateOptions{}) Create(context.TODO(), newDeployment, metav1.CreateOptions{})
if err != nil { if err != nil {
return NoSync, err return NoSync, err
} }
c.ConnectionPooler[role].Deployment = deployment c.ConnectionPooler[role].Deployment = deployment
} else if err != nil { } else if err != nil {
msg := "could not get connection pooler deployment to sync: %v" return NoSync, fmt.Errorf("could not get connection pooler deployment to sync: %v", err)
return NoSync, fmt.Errorf(msg, err)
} else { } else {
c.ConnectionPooler[role].Deployment = deployment c.ConnectionPooler[role].Deployment = deployment
// actual synchronization // actual synchronization
@@ -865,25 +877,24 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql
if oldSpec != nil { if oldSpec != nil {
specSync, specReason = needSyncConnectionPoolerSpecs(oldConnectionPooler, newConnectionPooler, c.logger) specSync, specReason = needSyncConnectionPoolerSpecs(oldConnectionPooler, newConnectionPooler, c.logger)
syncReason = append(syncReason, specReason...)
} }
defaultsSync, defaultsReason := c.needSyncConnectionPoolerDefaults(&c.Config, newConnectionPooler, deployment) defaultsSync, defaultsReason := c.needSyncConnectionPoolerDefaults(&c.Config, newConnectionPooler, deployment)
reason := append(specReason, defaultsReason...) syncReason = append(syncReason, defaultsReason...)
if specSync || defaultsSync { if specSync || defaultsSync {
c.logger.Infof("Update connection pooler deployment %s, reason: %+v", c.logger.Infof("update connection pooler deployment %s, reason: %+v",
c.connectionPoolerName(role), reason) c.connectionPoolerName(role), syncReason)
newDeploymentSpec, err := c.generateConnectionPoolerDeployment(c.ConnectionPooler[role]) newDeployment, err = c.generateConnectionPoolerDeployment(c.ConnectionPooler[role])
if err != nil { if err != nil {
msg := "could not generate deployment for connection pooler: %v" return syncReason, fmt.Errorf("could not generate deployment for connection pooler: %v", err)
return reason, fmt.Errorf(msg, err)
} }
deployment, err := updateConnectionPoolerDeployment(c.KubeClient, deployment, err = updateConnectionPoolerDeployment(c.KubeClient, newDeployment)
newDeploymentSpec)
if err != nil { if err != nil {
return reason, err return syncReason, err
} }
c.ConnectionPooler[role].Deployment = deployment c.ConnectionPooler[role].Deployment = deployment
} }
@@ -898,31 +909,38 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql
c.ConnectionPooler[role].Deployment = deployment c.ConnectionPooler[role].Deployment = deployment
} }
service, err := c.KubeClient. if service, err = c.KubeClient.Services(c.Namespace).Get(context.TODO(), c.connectionPoolerName(role), metav1.GetOptions{}); err == nil {
Services(c.Namespace). c.ConnectionPooler[role].Service = service
Get(context.TODO(), c.connectionPoolerName(role), metav1.GetOptions{}) desiredSvc := c.generateConnectionPoolerService(c.ConnectionPooler[role])
if match, reason := c.compareServices(service, desiredSvc); !match {
if err != nil && k8sutil.ResourceNotFound(err) { syncReason = append(syncReason, reason)
msg := "Service %s for connection pooler synchronization is not found, create it" c.logServiceChanges(role, service, desiredSvc, false, reason)
c.logger.Warningf(msg, c.connectionPoolerName(role)) newService, err = c.updateService(role, service, desiredSvc)
if err != nil {
serviceSpec := c.generateConnectionPoolerService(c.ConnectionPooler[role]) return syncReason, fmt.Errorf("could not update %s service to match desired state: %v", role, err)
service, err := c.KubeClient. }
Services(serviceSpec.Namespace). c.ConnectionPooler[role].Service = newService
Create(context.TODO(), serviceSpec, metav1.CreateOptions{}) c.logger.Infof("%s service %q is in the desired state now", role, util.NameFromMeta(desiredSvc.ObjectMeta))
if err != nil {
return NoSync, err
} }
c.ConnectionPooler[role].Service = service return NoSync, nil
} else if err != nil {
msg := "could not get connection pooler service to sync: %v"
return NoSync, fmt.Errorf(msg, err)
} else {
// Service updates are not supported and probably not that useful anyway
c.ConnectionPooler[role].Service = service
} }
if !k8sutil.ResourceNotFound(err) {
return NoSync, fmt.Errorf("could not get connection pooler service to sync: %v", err)
}
c.ConnectionPooler[role].Service = nil
c.logger.Warningf("service %s for connection pooler synchronization is not found, create it", c.connectionPoolerName(role))
serviceSpec := c.generateConnectionPoolerService(c.ConnectionPooler[role])
newService, err = c.KubeClient.
Services(serviceSpec.Namespace).
Create(context.TODO(), serviceSpec, metav1.CreateOptions{})
if err != nil {
return NoSync, err
}
c.ConnectionPooler[role].Service = newService
return NoSync, nil return NoSync, nil
} }
+4 -8
View File
@@ -27,10 +27,6 @@ func boolToPointer(value bool) *bool {
return &value return &value
} }
func int32ToPointer(value int32) *int32 {
return &value
}
func deploymentUpdated(cluster *Cluster, err error, reason SyncReason) error { func deploymentUpdated(cluster *Cluster, err error, reason SyncReason) error {
for _, role := range [2]PostgresRole{Master, Replica} { for _, role := range [2]PostgresRole{Master, Replica} {
@@ -294,7 +290,7 @@ func TestConnectionPoolerCreateDeletion(t *testing.T) {
ConnectionPoolerDefaultCPULimit: "100m", ConnectionPoolerDefaultCPULimit: "100m",
ConnectionPoolerDefaultMemoryRequest: "100Mi", ConnectionPoolerDefaultMemoryRequest: "100Mi",
ConnectionPoolerDefaultMemoryLimit: "100Mi", ConnectionPoolerDefaultMemoryLimit: "100Mi",
NumberOfInstances: int32ToPointer(1), NumberOfInstances: k8sutil.Int32ToPointer(1),
}, },
PodManagementPolicy: "ordered_ready", PodManagementPolicy: "ordered_ready",
Resources: config.Resources{ Resources: config.Resources{
@@ -401,7 +397,7 @@ func TestConnectionPoolerSync(t *testing.T) {
ConnectionPoolerDefaultCPULimit: "100m", ConnectionPoolerDefaultCPULimit: "100m",
ConnectionPoolerDefaultMemoryRequest: "100Mi", ConnectionPoolerDefaultMemoryRequest: "100Mi",
ConnectionPoolerDefaultMemoryLimit: "100Mi", ConnectionPoolerDefaultMemoryLimit: "100Mi",
NumberOfInstances: int32ToPointer(1), NumberOfInstances: k8sutil.Int32ToPointer(1),
}, },
PodManagementPolicy: "ordered_ready", PodManagementPolicy: "ordered_ready",
Resources: config.Resources{ Resources: config.Resources{
@@ -639,7 +635,7 @@ func TestConnectionPoolerSync(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
tt.cluster.OpConfig.ConnectionPooler.Image = tt.defaultImage tt.cluster.OpConfig.ConnectionPooler.Image = tt.defaultImage
tt.cluster.OpConfig.ConnectionPooler.NumberOfInstances = tt.cluster.OpConfig.ConnectionPooler.NumberOfInstances =
int32ToPointer(tt.defaultInstances) k8sutil.Int32ToPointer(tt.defaultInstances)
t.Logf("running test for %s [%s]", testName, tt.subTest) t.Logf("running test for %s [%s]", testName, tt.subTest)
@@ -664,7 +660,7 @@ func TestConnectionPoolerPodSpec(t *testing.T) {
ReplicationUsername: replicationUserName, ReplicationUsername: replicationUserName,
}, },
ConnectionPooler: config.ConnectionPooler{ ConnectionPooler: config.ConnectionPooler{
MaxDBConnections: int32ToPointer(60), MaxDBConnections: k8sutil.Int32ToPointer(60),
ConnectionPoolerDefaultCPURequest: "100m", ConnectionPoolerDefaultCPURequest: "100m",
ConnectionPoolerDefaultCPULimit: "100m", ConnectionPoolerDefaultCPULimit: "100m",
ConnectionPoolerDefaultMemoryRequest: "100Mi", ConnectionPoolerDefaultMemoryRequest: "100Mi",
+149 -128
View File
@@ -37,6 +37,8 @@ const (
patroniPGBinariesParameterName = "bin_dir" patroniPGBinariesParameterName = "bin_dir"
patroniPGHBAConfParameterName = "pg_hba" patroniPGHBAConfParameterName = "pg_hba"
localHost = "127.0.0.1/32" localHost = "127.0.0.1/32"
scalyrSidecarName = "scalyr-sidecar"
logicalBackupContainerName = "logical-backup"
connectionPoolerContainer = "connection-pooler" connectionPoolerContainer = "connection-pooler"
pgPort = 5432 pgPort = 5432
) )
@@ -102,24 +104,22 @@ func (c *Cluster) serviceAddress(role PostgresRole) string {
return "" return ""
} }
func (c *Cluster) servicePort(role PostgresRole) string { func (c *Cluster) servicePort(role PostgresRole) int32 {
service, exist := c.Services[role] service, exist := c.Services[role]
if exist { if exist {
return fmt.Sprint(service.Spec.Ports[0].Port) return service.Spec.Ports[0].Port
} }
c.logger.Warningf("No service for role %s", role) c.logger.Warningf("No service for role %s - defaulting to port 5432", role)
return "" return pgPort
} }
func (c *Cluster) podDisruptionBudgetName() string { func (c *Cluster) podDisruptionBudgetName() string {
return c.OpConfig.PDBNameFormat.Format("cluster", c.Name) return c.OpConfig.PDBNameFormat.Format("cluster", c.Name)
} }
func (c *Cluster) makeDefaultResources() acidv1.Resources { func makeDefaultResources(config *config.Config) acidv1.Resources {
config := c.OpConfig
defaultRequests := acidv1.ResourceDescription{ defaultRequests := acidv1.ResourceDescription{
CPU: config.Resources.DefaultCPURequest, CPU: config.Resources.DefaultCPURequest,
@@ -136,25 +136,61 @@ func (c *Cluster) makeDefaultResources() acidv1.Resources {
} }
} }
func generateResourceRequirements(resources acidv1.Resources, defaultResources acidv1.Resources) (*v1.ResourceRequirements, error) { func (c *Cluster) enforceMinResourceLimits(resources *v1.ResourceRequirements) error {
var err error var (
isSmaller bool
err error
msg string
)
specRequests := resources.ResourceRequests // setting limits too low can cause unnecessary evictions / OOM kills
specLimits := resources.ResourceLimits cpuLimit := resources.Limits[v1.ResourceCPU]
minCPULimit := c.OpConfig.MinCPULimit
result := v1.ResourceRequirements{} if minCPULimit != "" {
isSmaller, err = util.IsSmallerQuantity(cpuLimit.String(), minCPULimit)
result.Requests, err = fillResourceList(specRequests, defaultResources.ResourceRequests) if err != nil {
if err != nil { return fmt.Errorf("could not compare defined CPU limit %s for %q container with configured minimum value %s: %v",
return nil, fmt.Errorf("could not fill resource requests: %v", err) cpuLimit.String(), constants.PostgresContainerName, minCPULimit, err)
}
if isSmaller {
msg = fmt.Sprintf("defined CPU limit %s for %q container is below required minimum %s and will be increased",
cpuLimit.String(), constants.PostgresContainerName, minCPULimit)
c.logger.Warningf(msg)
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "ResourceLimits", msg)
resources.Limits[v1.ResourceCPU], _ = resource.ParseQuantity(minCPULimit)
}
} }
result.Limits, err = fillResourceList(specLimits, defaultResources.ResourceLimits) memoryLimit := resources.Limits[v1.ResourceMemory]
if err != nil { minMemoryLimit := c.OpConfig.MinMemoryLimit
return nil, fmt.Errorf("could not fill resource limits: %v", err) if minMemoryLimit != "" {
isSmaller, err = util.IsSmallerQuantity(memoryLimit.String(), minMemoryLimit)
if err != nil {
return fmt.Errorf("could not compare defined memory limit %s for %q container with configured minimum value %s: %v",
memoryLimit.String(), constants.PostgresContainerName, minMemoryLimit, err)
}
if isSmaller {
msg = fmt.Sprintf("defined memory limit %s for %q container is below required minimum %s and will be increased",
memoryLimit.String(), constants.PostgresContainerName, minMemoryLimit)
c.logger.Warningf(msg)
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "ResourceLimits", msg)
resources.Limits[v1.ResourceMemory], _ = resource.ParseQuantity(minMemoryLimit)
}
} }
return &result, nil return nil
}
func setMemoryRequestToLimit(resources *v1.ResourceRequirements, containerName string, logger *logrus.Entry) {
requests := resources.Requests[v1.ResourceMemory]
limits := resources.Limits[v1.ResourceMemory]
isSmaller := requests.Cmp(limits) == -1
if isSmaller {
logger.Warningf("memory request of %s for %q container is increased to match memory limit of %s",
requests.String(), containerName, limits.String())
resources.Requests[v1.ResourceMemory] = limits
}
} }
func fillResourceList(spec acidv1.ResourceDescription, defaults acidv1.ResourceDescription) (v1.ResourceList, error) { func fillResourceList(spec acidv1.ResourceDescription, defaults acidv1.ResourceDescription) (v1.ResourceList, error) {
@@ -187,6 +223,44 @@ func fillResourceList(spec acidv1.ResourceDescription, defaults acidv1.ResourceD
return requests, nil return requests, nil
} }
func (c *Cluster) generateResourceRequirements(
resources *acidv1.Resources,
defaultResources acidv1.Resources,
containerName string) (*v1.ResourceRequirements, error) {
var err error
specRequests := acidv1.ResourceDescription{}
specLimits := acidv1.ResourceDescription{}
result := v1.ResourceRequirements{}
if resources != nil {
specRequests = resources.ResourceRequests
specLimits = resources.ResourceLimits
}
result.Requests, err = fillResourceList(specRequests, defaultResources.ResourceRequests)
if err != nil {
return nil, fmt.Errorf("could not fill resource requests: %v", err)
}
result.Limits, err = fillResourceList(specLimits, defaultResources.ResourceLimits)
if err != nil {
return nil, fmt.Errorf("could not fill resource limits: %v", err)
}
// enforce minimum cpu and memory limits for Postgres containers only
if containerName == constants.PostgresContainerName {
if err = c.enforceMinResourceLimits(&result); err != nil {
return nil, fmt.Errorf("could not enforce minimum resource limits: %v", err)
}
}
if c.OpConfig.SetMemoryRequestToLimit {
setMemoryRequestToLimit(&result, containerName, c.logger)
}
return &result, nil
}
func generateSpiloJSONConfiguration(pg *acidv1.PostgresqlParam, patroni *acidv1.Patroni, pamRoleName string, EnablePgVersionEnvVar bool, logger *logrus.Entry) (string, error) { func generateSpiloJSONConfiguration(pg *acidv1.PostgresqlParam, patroni *acidv1.Patroni, pamRoleName string, EnablePgVersionEnvVar bool, logger *logrus.Entry) (string, error) {
config := spiloConfiguration{} config := spiloConfiguration{}
@@ -507,22 +581,20 @@ func generateContainer(
} }
} }
func generateSidecarContainers(sidecars []acidv1.Sidecar, func (c *Cluster) generateSidecarContainers(sidecars []acidv1.Sidecar,
defaultResources acidv1.Resources, startIndex int, logger *logrus.Entry) ([]v1.Container, error) { defaultResources acidv1.Resources, startIndex int) ([]v1.Container, error) {
if len(sidecars) > 0 { if len(sidecars) > 0 {
result := make([]v1.Container, 0) result := make([]v1.Container, 0)
for index, sidecar := range sidecars { for index, sidecar := range sidecars {
var resourcesSpec acidv1.Resources
if sidecar.Resources == nil {
resourcesSpec = acidv1.Resources{}
} else {
sidecar.Resources.DeepCopyInto(&resourcesSpec)
}
resources, err := generateResourceRequirements( resources, err := c.generateResourceRequirements(&resourcesSpec, defaultResources, sidecar.Name)
makeResources(
sidecar.Resources.ResourceRequests.CPU,
sidecar.Resources.ResourceRequests.Memory,
sidecar.Resources.ResourceLimits.CPU,
sidecar.Resources.ResourceLimits.Memory,
),
defaultResources,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -997,63 +1069,9 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
additionalVolumes = spec.AdditionalVolumes additionalVolumes = spec.AdditionalVolumes
) )
// Improve me. Please. defaultResources := makeDefaultResources(&c.OpConfig)
if c.OpConfig.SetMemoryRequestToLimit { resourceRequirements, err := c.generateResourceRequirements(
spec.Resources, defaultResources, constants.PostgresContainerName)
// controller adjusts the default memory request at operator startup
request := spec.Resources.ResourceRequests.Memory
if request == "" {
request = c.OpConfig.Resources.DefaultMemoryRequest
}
limit := spec.Resources.ResourceLimits.Memory
if limit == "" {
limit = c.OpConfig.Resources.DefaultMemoryLimit
}
isSmaller, err := util.IsSmallerQuantity(request, limit)
if err != nil {
return nil, err
}
if isSmaller {
c.logger.Warningf("The memory request of %v for the Postgres container is increased to match the memory limit of %v.", request, limit)
spec.Resources.ResourceRequests.Memory = limit
}
// controller adjusts the Scalyr sidecar request at operator startup
// as this sidecar is managed separately
// adjust sidecar containers defined for that particular cluster
for _, sidecar := range spec.Sidecars {
// TODO #413
sidecarRequest := sidecar.Resources.ResourceRequests.Memory
if request == "" {
request = c.OpConfig.Resources.DefaultMemoryRequest
}
sidecarLimit := sidecar.Resources.ResourceLimits.Memory
if limit == "" {
limit = c.OpConfig.Resources.DefaultMemoryLimit
}
isSmaller, err := util.IsSmallerQuantity(sidecarRequest, sidecarLimit)
if err != nil {
return nil, err
}
if isSmaller {
c.logger.Warningf("The memory request of %v for the %v sidecar container is increased to match the memory limit of %v.", sidecar.Resources.ResourceRequests.Memory, sidecar.Name, sidecar.Resources.ResourceLimits.Memory)
sidecar.Resources.ResourceRequests.Memory = sidecar.Resources.ResourceLimits.Memory
}
}
}
defaultResources := c.makeDefaultResources()
resourceRequirements, err := generateResourceRequirements(spec.Resources, defaultResources)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not generate resource requirements: %v", err) return nil, fmt.Errorf("could not generate resource requirements: %v", err)
} }
@@ -1101,7 +1119,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
// backward compatible check for InitContainers // backward compatible check for InitContainers
if spec.InitContainersOld != nil { if spec.InitContainersOld != nil {
msg := "Manifest parameter init_containers is deprecated." msg := "manifest parameter init_containers is deprecated."
if spec.InitContainers == nil { if spec.InitContainers == nil {
c.logger.Warningf("%s Consider using initContainers instead.", msg) c.logger.Warningf("%s Consider using initContainers instead.", msg)
spec.InitContainers = spec.InitContainersOld spec.InitContainers = spec.InitContainersOld
@@ -1112,7 +1130,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
// backward compatible check for PodPriorityClassName // backward compatible check for PodPriorityClassName
if spec.PodPriorityClassNameOld != "" { if spec.PodPriorityClassNameOld != "" {
msg := "Manifest parameter pod_priority_class_name is deprecated." msg := "manifest parameter pod_priority_class_name is deprecated."
if spec.PodPriorityClassName == "" { if spec.PodPriorityClassName == "" {
c.logger.Warningf("%s Consider using podPriorityClassName instead.", msg) c.logger.Warningf("%s Consider using podPriorityClassName instead.", msg)
spec.PodPriorityClassName = spec.PodPriorityClassNameOld spec.PodPriorityClassName = spec.PodPriorityClassNameOld
@@ -1230,7 +1248,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
c.logger.Warningf("sidecars specified but disabled in configuration - next statefulset creation would fail") c.logger.Warningf("sidecars specified but disabled in configuration - next statefulset creation would fail")
} }
if clusterSpecificSidecars, err = generateSidecarContainers(spec.Sidecars, defaultResources, 0, c.logger); err != nil { if clusterSpecificSidecars, err = c.generateSidecarContainers(spec.Sidecars, defaultResources, 0); err != nil {
return nil, fmt.Errorf("could not generate sidecar containers: %v", err) return nil, fmt.Errorf("could not generate sidecar containers: %v", err)
} }
} }
@@ -1241,7 +1259,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
for name, dockerImage := range c.OpConfig.SidecarImages { for name, dockerImage := range c.OpConfig.SidecarImages {
globalSidecarsByDockerImage = append(globalSidecarsByDockerImage, acidv1.Sidecar{Name: name, DockerImage: dockerImage}) globalSidecarsByDockerImage = append(globalSidecarsByDockerImage, acidv1.Sidecar{Name: name, DockerImage: dockerImage})
} }
if globalSidecarContainersByDockerImage, err = generateSidecarContainers(globalSidecarsByDockerImage, defaultResources, len(clusterSpecificSidecars), c.logger); err != nil { if globalSidecarContainersByDockerImage, err = c.generateSidecarContainers(globalSidecarsByDockerImage, defaultResources, len(clusterSpecificSidecars)); err != nil {
return nil, fmt.Errorf("could not generate sidecar containers: %v", err) return nil, fmt.Errorf("could not generate sidecar containers: %v", err)
} }
// make the resulting list reproducible // make the resulting list reproducible
@@ -1254,7 +1272,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
// generate scalyr sidecar container // generate scalyr sidecar container
var scalyrSidecars []v1.Container var scalyrSidecars []v1.Container
if scalyrSidecar, err := if scalyrSidecar, err :=
generateScalyrSidecarSpec(c.Name, c.generateScalyrSidecarSpec(c.Name,
c.OpConfig.ScalyrAPIKey, c.OpConfig.ScalyrAPIKey,
c.OpConfig.ScalyrServerURL, c.OpConfig.ScalyrServerURL,
c.OpConfig.ScalyrImage, c.OpConfig.ScalyrImage,
@@ -1262,8 +1280,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
c.OpConfig.ScalyrMemoryRequest, c.OpConfig.ScalyrMemoryRequest,
c.OpConfig.ScalyrCPULimit, c.OpConfig.ScalyrCPULimit,
c.OpConfig.ScalyrMemoryLimit, c.OpConfig.ScalyrMemoryLimit,
defaultResources, defaultResources); err != nil {
c.logger); err != nil {
return nil, fmt.Errorf("could not generate Scalyr sidecar: %v", err) return nil, fmt.Errorf("could not generate Scalyr sidecar: %v", err)
} else { } else {
if scalyrSidecar != nil { if scalyrSidecar != nil {
@@ -1373,12 +1390,12 @@ func (c *Cluster) generatePodAnnotations(spec *acidv1.PostgresSpec) map[string]s
return annotations return annotations
} }
func generateScalyrSidecarSpec(clusterName, APIKey, serverURL, dockerImage string, func (c *Cluster) generateScalyrSidecarSpec(clusterName, APIKey, serverURL, dockerImage string,
scalyrCPURequest string, scalyrMemoryRequest string, scalyrCPULimit string, scalyrMemoryLimit string, scalyrCPURequest string, scalyrMemoryRequest string, scalyrCPULimit string, scalyrMemoryLimit string,
defaultResources acidv1.Resources, logger *logrus.Entry) (*v1.Container, error) { defaultResources acidv1.Resources) (*v1.Container, error) {
if APIKey == "" || dockerImage == "" { if APIKey == "" || dockerImage == "" {
if APIKey == "" && dockerImage != "" { if APIKey == "" && dockerImage != "" {
logger.Warning("Not running Scalyr sidecar: SCALYR_API_KEY must be defined") c.logger.Warning("Not running Scalyr sidecar: SCALYR_API_KEY must be defined")
} }
return nil, nil return nil, nil
} }
@@ -1388,7 +1405,8 @@ func generateScalyrSidecarSpec(clusterName, APIKey, serverURL, dockerImage strin
scalyrCPULimit, scalyrCPULimit,
scalyrMemoryLimit, scalyrMemoryLimit,
) )
resourceRequirementsScalyrSidecar, err := generateResourceRequirements(resourcesScalyrSidecar, defaultResources) resourceRequirementsScalyrSidecar, err := c.generateResourceRequirements(
&resourcesScalyrSidecar, defaultResources, scalyrSidecarName)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid resources for Scalyr sidecar: %v", err) return nil, fmt.Errorf("invalid resources for Scalyr sidecar: %v", err)
} }
@@ -1406,7 +1424,7 @@ func generateScalyrSidecarSpec(clusterName, APIKey, serverURL, dockerImage strin
env = append(env, v1.EnvVar{Name: "SCALYR_SERVER_URL", Value: serverURL}) env = append(env, v1.EnvVar{Name: "SCALYR_SERVER_URL", Value: serverURL})
} }
return &v1.Container{ return &v1.Container{
Name: "scalyr-sidecar", Name: scalyrSidecarName,
Image: dockerImage, Image: dockerImage,
Env: env, Env: env,
ImagePullPolicy: v1.PullIfNotPresent, ImagePullPolicy: v1.PullIfNotPresent,
@@ -1505,13 +1523,13 @@ func (c *Cluster) addAdditionalVolumes(podSpec *v1.PodSpec,
mountPaths := map[string]acidv1.AdditionalVolume{} mountPaths := map[string]acidv1.AdditionalVolume{}
for i, additionalVolume := range additionalVolumes { for i, additionalVolume := range additionalVolumes {
if previousVolume, exist := mountPaths[additionalVolume.MountPath]; exist { if previousVolume, exist := mountPaths[additionalVolume.MountPath]; exist {
msg := "Volume %+v cannot be mounted to the same path as %+v" msg := "volume %+v cannot be mounted to the same path as %+v"
c.logger.Warningf(msg, additionalVolume, previousVolume) c.logger.Warningf(msg, additionalVolume, previousVolume)
continue continue
} }
if additionalVolume.MountPath == constants.PostgresDataMount { if additionalVolume.MountPath == constants.PostgresDataMount {
msg := "Cannot mount volume on postgresql data directory, %+v" msg := "cannot mount volume on postgresql data directory, %+v"
c.logger.Warningf(msg, additionalVolume) c.logger.Warningf(msg, additionalVolume)
continue continue
} }
@@ -1524,7 +1542,7 @@ func (c *Cluster) addAdditionalVolumes(podSpec *v1.PodSpec,
for _, target := range additionalVolume.TargetContainers { for _, target := range additionalVolume.TargetContainers {
if target == "all" && len(additionalVolume.TargetContainers) != 1 { if target == "all" && len(additionalVolume.TargetContainers) != 1 {
msg := `Target containers could be either "all" or a list msg := `target containers could be either "all" or a list
of containers, mixing those is not allowed, %+v` of containers, mixing those is not allowed, %+v`
c.logger.Warningf(msg, additionalVolume) c.logger.Warningf(msg, additionalVolume)
continue continue
@@ -1623,7 +1641,7 @@ func (c *Cluster) generateUserSecrets() map[string]*v1.Secret {
func (c *Cluster) generateSingleUserSecret(namespace string, pgUser spec.PgUser) *v1.Secret { func (c *Cluster) generateSingleUserSecret(namespace string, pgUser spec.PgUser) *v1.Secret {
//Skip users with no password i.e. human users (they'll be authenticated using pam) //Skip users with no password i.e. human users (they'll be authenticated using pam)
if pgUser.Password == "" { if pgUser.Password == "" {
if pgUser.Origin != spec.RoleOriginTeamsAPI { if pgUser.Origin != spec.RoleOriginTeamsAPI && pgUser.Origin != spec.RoleOriginSpilo {
c.logger.Warningf("could not generate secret for a non-teamsAPI role %q: role has no password", c.logger.Warningf("could not generate secret for a non-teamsAPI role %q: role has no password",
pgUser.Name) pgUser.Name)
} }
@@ -1700,23 +1718,7 @@ func (c *Cluster) generateService(role PostgresRole, spec *acidv1.PostgresSpec)
} }
if c.shouldCreateLoadBalancerForService(role, spec) { if c.shouldCreateLoadBalancerForService(role, spec) {
c.configureLoadBalanceService(&serviceSpec, spec.AllowedSourceRanges)
// spec.AllowedSourceRanges evaluates to the empty slice of zero length
// when omitted or set to 'null'/empty sequence in the PG manifest
if len(spec.AllowedSourceRanges) > 0 {
serviceSpec.LoadBalancerSourceRanges = spec.AllowedSourceRanges
} else {
// safe default value: lock a load balancer only to the local address unless overridden explicitly
serviceSpec.LoadBalancerSourceRanges = []string{localHost}
}
c.logger.Debugf("final load balancer source ranges as seen in a service spec (not necessarily applied): %q", serviceSpec.LoadBalancerSourceRanges)
serviceSpec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyType(c.OpConfig.ExternalTrafficPolicy)
serviceSpec.Type = v1.ServiceTypeLoadBalancer
} else if role == Replica {
// before PR #258, the replica service was only created if allocated a LB
// now we always create the service but warn if the LB is absent
c.logger.Debugf("No load balancer created for the replica service")
} }
service := &v1.Service{ service := &v1.Service{
@@ -1732,6 +1734,21 @@ func (c *Cluster) generateService(role PostgresRole, spec *acidv1.PostgresSpec)
return service return service
} }
func (c *Cluster) configureLoadBalanceService(serviceSpec *v1.ServiceSpec, sourceRanges []string) {
// spec.AllowedSourceRanges evaluates to the empty slice of zero length
// when omitted or set to 'null'/empty sequence in the PG manifest
if len(sourceRanges) > 0 {
serviceSpec.LoadBalancerSourceRanges = sourceRanges
} else {
// safe default value: lock a load balancer only to the local address unless overridden explicitly
serviceSpec.LoadBalancerSourceRanges = []string{localHost}
}
c.logger.Debugf("final load balancer source ranges as seen in a service spec (not necessarily applied): %q", serviceSpec.LoadBalancerSourceRanges)
serviceSpec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyType(c.OpConfig.ExternalTrafficPolicy)
serviceSpec.Type = v1.ServiceTypeLoadBalancer
}
func (c *Cluster) generateServiceAnnotations(role PostgresRole, spec *acidv1.PostgresSpec) map[string]string { func (c *Cluster) generateServiceAnnotations(role PostgresRole, spec *acidv1.PostgresSpec) map[string]string {
annotations := make(map[string]string) annotations := make(map[string]string)
@@ -1815,11 +1832,11 @@ func (c *Cluster) generateCloneEnvironment(description *acidv1.CloneDescription)
}) })
} else { } else {
// cloning with S3, find out the bucket to clone // cloning with S3, find out the bucket to clone
msg := "Clone from S3 bucket" msg := "clone from S3 bucket"
c.logger.Info(msg, description.S3WalPath) c.logger.Info(msg, description.S3WalPath)
if description.S3WalPath == "" { if description.S3WalPath == "" {
msg := "Figure out which S3 bucket to use from env" msg := "figure out which S3 bucket to use from env"
c.logger.Info(msg, description.S3WalPath) c.logger.Info(msg, description.S3WalPath)
if c.OpConfig.WALES3Bucket != "" { if c.OpConfig.WALES3Bucket != "" {
@@ -1863,7 +1880,7 @@ func (c *Cluster) generateCloneEnvironment(description *acidv1.CloneDescription)
result = append(result, envs...) result = append(result, envs...)
} else { } else {
msg := "Use custom parsed S3WalPath %s from the manifest" msg := "use custom parsed S3WalPath %s from the manifest"
c.logger.Warningf(msg, description.S3WalPath) c.logger.Warningf(msg, description.S3WalPath)
result = append(result, v1.EnvVar{ result = append(result, v1.EnvVar{
@@ -2000,15 +2017,15 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1beta1.CronJob, error) {
c.logger.Debug("Generating logical backup pod template") c.logger.Debug("Generating logical backup pod template")
// allocate for the backup pod the same amount of resources as for normal DB pods // allocate for the backup pod the same amount of resources as for normal DB pods
defaultResources := c.makeDefaultResources() resourceRequirements, err = c.generateResourceRequirements(
resourceRequirements, err = generateResourceRequirements(c.Spec.Resources, defaultResources) c.Spec.Resources, makeDefaultResources(&c.OpConfig), logicalBackupContainerName)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not generate resource requirements for logical backup pods: %v", err) return nil, fmt.Errorf("could not generate resource requirements for logical backup pods: %v", err)
} }
envVars := c.generateLogicalBackupPodEnvVars() envVars := c.generateLogicalBackupPodEnvVars()
logicalBackupContainer := generateContainer( logicalBackupContainer := generateContainer(
"logical-backup", logicalBackupContainerName,
&c.OpConfig.LogicalBackup.LogicalBackupDockerImage, &c.OpConfig.LogicalBackup.LogicalBackupDockerImage,
resourceRequirements, resourceRequirements,
envVars, envVars,
@@ -2145,6 +2162,10 @@ func (c *Cluster) generateLogicalBackupPodEnvVars() []v1.EnvVar {
Name: "LOGICAL_BACKUP_S3_SSE", Name: "LOGICAL_BACKUP_S3_SSE",
Value: c.OpConfig.LogicalBackup.LogicalBackupS3SSE, Value: c.OpConfig.LogicalBackup.LogicalBackupS3SSE,
}, },
{
Name: "LOGICAL_BACKUP_S3_RETENTION_TIME",
Value: c.OpConfig.LogicalBackup.LogicalBackupS3RetentionTime,
},
{ {
Name: "LOGICAL_BACKUP_S3_BUCKET_SCOPE_SUFFIX", Name: "LOGICAL_BACKUP_S3_BUCKET_SCOPE_SUFFIX",
Value: getBucketScopeSuffix(string(c.Postgresql.GetUID())), Value: getBucketScopeSuffix(string(c.Postgresql.GetUID())),
+505 -8
View File
@@ -27,8 +27,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
v1core "k8s.io/client-go/kubernetes/typed/core/v1" v1core "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/record"
) )
func newFakeK8sTestClient() (k8sutil.KubernetesClient, *fake.Clientset) { func newFakeK8sTestClient() (k8sutil.KubernetesClient, *fake.Clientset) {
@@ -991,7 +993,7 @@ func TestNodeAffinity(t *testing.T) {
makeSpec := func(nodeAffinity *v1.NodeAffinity) acidv1.PostgresSpec { makeSpec := func(nodeAffinity *v1.NodeAffinity) acidv1.PostgresSpec {
return acidv1.PostgresSpec{ return acidv1.PostgresSpec{
TeamID: "myapp", NumberOfInstances: 1, TeamID: "myapp", NumberOfInstances: 1,
Resources: acidv1.Resources{ Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"}, ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"}, ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
}, },
@@ -1087,7 +1089,7 @@ func TestTLS(t *testing.T) {
}, },
Spec: acidv1.PostgresSpec{ Spec: acidv1.PostgresSpec{
TeamID: "myapp", NumberOfInstances: 1, TeamID: "myapp", NumberOfInstances: 1,
Resources: acidv1.Resources{ Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"}, ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"}, ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
}, },
@@ -1206,7 +1208,7 @@ func TestAdditionalVolume(t *testing.T) {
}, },
Spec: acidv1.PostgresSpec{ Spec: acidv1.PostgresSpec{
TeamID: "myapp", NumberOfInstances: 1, TeamID: "myapp", NumberOfInstances: 1,
Resources: acidv1.Resources{ Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"}, ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"}, ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
}, },
@@ -1312,7 +1314,7 @@ func TestSidecars(t *testing.T) {
}, },
}, },
TeamID: "myapp", NumberOfInstances: 1, TeamID: "myapp", NumberOfInstances: 1,
Resources: acidv1.Resources{ Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"}, ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"}, ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
}, },
@@ -1325,7 +1327,7 @@ func TestSidecars(t *testing.T) {
}, },
acidv1.Sidecar{ acidv1.Sidecar{
Name: "cluster-specific-sidecar-with-resources", Name: "cluster-specific-sidecar-with-resources",
Resources: acidv1.Resources{ Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "210m", Memory: "0.8Gi"}, ResourceRequests: acidv1.ResourceDescription{CPU: "210m", Memory: "0.8Gi"},
ResourceLimits: acidv1.ResourceDescription{CPU: "510m", Memory: "1.4Gi"}, ResourceLimits: acidv1.ResourceDescription{CPU: "510m", Memory: "1.4Gi"},
}, },
@@ -1487,7 +1489,7 @@ func TestGenerateService(t *testing.T) {
var enableLB bool = true var enableLB bool = true
spec = acidv1.PostgresSpec{ spec = acidv1.PostgresSpec{
TeamID: "myapp", NumberOfInstances: 1, TeamID: "myapp", NumberOfInstances: 1,
Resources: acidv1.Resources{ Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"}, ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"}, ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
}, },
@@ -1500,7 +1502,7 @@ func TestGenerateService(t *testing.T) {
}, },
acidv1.Sidecar{ acidv1.Sidecar{
Name: "cluster-specific-sidecar-with-resources", Name: "cluster-specific-sidecar-with-resources",
Resources: acidv1.Resources{ Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "210m", Memory: "0.8Gi"}, ResourceRequests: acidv1.ResourceDescription{CPU: "210m", Memory: "0.8Gi"},
ResourceLimits: acidv1.ResourceDescription{CPU: "510m", Memory: "1.4Gi"}, ResourceLimits: acidv1.ResourceDescription{CPU: "510m", Memory: "1.4Gi"},
}, },
@@ -1561,6 +1563,501 @@ func TestGenerateService(t *testing.T) {
} }
func newLBFakeClient() (k8sutil.KubernetesClient, *fake.Clientset) {
clientSet := fake.NewSimpleClientset()
return k8sutil.KubernetesClient{
DeploymentsGetter: clientSet.AppsV1(),
ServicesGetter: clientSet.CoreV1(),
}, clientSet
}
func getServices(serviceType v1.ServiceType, sourceRanges []string, extTrafficPolicy, clusterName string) []v1.ServiceSpec {
return []v1.ServiceSpec{
v1.ServiceSpec{
ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyType(extTrafficPolicy),
LoadBalancerSourceRanges: sourceRanges,
Ports: []v1.ServicePort{{Name: "postgresql", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}}},
Type: serviceType,
},
v1.ServiceSpec{
ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyType(extTrafficPolicy),
LoadBalancerSourceRanges: sourceRanges,
Ports: []v1.ServicePort{{Name: clusterName + "-pooler", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}}},
Selector: map[string]string{"connection-pooler": clusterName + "-pooler"},
Type: serviceType,
},
v1.ServiceSpec{
ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyType(extTrafficPolicy),
LoadBalancerSourceRanges: sourceRanges,
Ports: []v1.ServicePort{{Name: "postgresql", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}}},
Selector: map[string]string{"spilo-role": "replica", "application": "spilo", "cluster-name": clusterName},
Type: serviceType,
},
v1.ServiceSpec{
ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyType(extTrafficPolicy),
LoadBalancerSourceRanges: sourceRanges,
Ports: []v1.ServicePort{{Name: clusterName + "-pooler-repl", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}}},
Selector: map[string]string{"connection-pooler": clusterName + "-pooler-repl"},
Type: serviceType,
},
}
}
func TestEnableLoadBalancers(t *testing.T) {
testName := "Test enabling LoadBalancers"
client, _ := newLBFakeClient()
clusterName := "acid-test-cluster"
namespace := "default"
clusterNameLabel := "cluster-name"
roleLabel := "spilo-role"
roles := []PostgresRole{Master, Replica}
sourceRanges := []string{"192.186.1.2/22"}
extTrafficPolicy := "Cluster"
tests := []struct {
subTest string
config config.Config
pgSpec acidv1.Postgresql
expectedServices []v1.ServiceSpec
}{
{
subTest: "LBs enabled in config, disabled in manifest",
config: config.Config{
ConnectionPooler: config.ConnectionPooler{
ConnectionPoolerDefaultCPURequest: "100m",
ConnectionPoolerDefaultCPULimit: "100m",
ConnectionPoolerDefaultMemoryRequest: "100Mi",
ConnectionPoolerDefaultMemoryLimit: "100Mi",
NumberOfInstances: k8sutil.Int32ToPointer(1),
},
EnableMasterLoadBalancer: true,
EnableMasterPoolerLoadBalancer: true,
EnableReplicaLoadBalancer: true,
EnableReplicaPoolerLoadBalancer: true,
ExternalTrafficPolicy: extTrafficPolicy,
Resources: config.Resources{
ClusterLabels: map[string]string{"application": "spilo"},
ClusterNameLabel: clusterNameLabel,
PodRoleLabel: roleLabel,
},
},
pgSpec: acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: clusterName,
Namespace: namespace,
},
Spec: acidv1.PostgresSpec{
AllowedSourceRanges: sourceRanges,
EnableConnectionPooler: util.True(),
EnableReplicaConnectionPooler: util.True(),
EnableMasterLoadBalancer: util.False(),
EnableMasterPoolerLoadBalancer: util.False(),
EnableReplicaLoadBalancer: util.False(),
EnableReplicaPoolerLoadBalancer: util.False(),
NumberOfInstances: 1,
Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
},
TeamID: "acid",
Volume: acidv1.Volume{
Size: "1G",
},
},
},
expectedServices: getServices(v1.ServiceTypeClusterIP, nil, "", clusterName),
},
{
subTest: "LBs enabled in manifest, disabled in config",
config: config.Config{
ConnectionPooler: config.ConnectionPooler{
ConnectionPoolerDefaultCPURequest: "100m",
ConnectionPoolerDefaultCPULimit: "100m",
ConnectionPoolerDefaultMemoryRequest: "100Mi",
ConnectionPoolerDefaultMemoryLimit: "100Mi",
NumberOfInstances: k8sutil.Int32ToPointer(1),
},
EnableMasterLoadBalancer: false,
EnableMasterPoolerLoadBalancer: false,
EnableReplicaLoadBalancer: false,
EnableReplicaPoolerLoadBalancer: false,
ExternalTrafficPolicy: extTrafficPolicy,
Resources: config.Resources{
ClusterLabels: map[string]string{"application": "spilo"},
ClusterNameLabel: clusterNameLabel,
PodRoleLabel: roleLabel,
},
},
pgSpec: acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: clusterName,
Namespace: namespace,
},
Spec: acidv1.PostgresSpec{
AllowedSourceRanges: sourceRanges,
EnableConnectionPooler: util.True(),
EnableReplicaConnectionPooler: util.True(),
EnableMasterLoadBalancer: util.True(),
EnableMasterPoolerLoadBalancer: util.True(),
EnableReplicaLoadBalancer: util.True(),
EnableReplicaPoolerLoadBalancer: util.True(),
NumberOfInstances: 1,
Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
},
TeamID: "acid",
Volume: acidv1.Volume{
Size: "1G",
},
},
},
expectedServices: getServices(v1.ServiceTypeLoadBalancer, sourceRanges, extTrafficPolicy, clusterName),
},
}
for _, tt := range tests {
var cluster = New(
Config{
OpConfig: tt.config,
}, client, tt.pgSpec, logger, eventRecorder)
cluster.Name = clusterName
cluster.Namespace = namespace
cluster.ConnectionPooler = map[PostgresRole]*ConnectionPoolerObjects{}
generatedServices := make([]v1.ServiceSpec, 0)
for _, role := range roles {
cluster.syncService(role)
cluster.ConnectionPooler[role] = &ConnectionPoolerObjects{
Name: cluster.connectionPoolerName(role),
ClusterName: cluster.ClusterName,
Namespace: cluster.Namespace,
Role: role,
}
cluster.syncConnectionPoolerWorker(&tt.pgSpec, &tt.pgSpec, role)
generatedServices = append(generatedServices, cluster.Services[role].Spec)
generatedServices = append(generatedServices, cluster.ConnectionPooler[role].Service.Spec)
}
if !reflect.DeepEqual(tt.expectedServices, generatedServices) {
t.Errorf("%s %s: expected %#v but got %#v", testName, tt.subTest, tt.expectedServices, generatedServices)
}
}
}
func TestGenerateResourceRequirements(t *testing.T) {
testName := "TestGenerateResourceRequirements"
client, _ := newFakeK8sTestClient()
clusterName := "acid-test-cluster"
namespace := "default"
clusterNameLabel := "cluster-name"
roleLabel := "spilo-role"
sidecarName := "postgres-exporter"
// two test cases will call enforceMinResourceLimits which emits 2 events per call
// hence bufferSize of 4 is required
newEventRecorder := record.NewFakeRecorder(4)
configResources := config.Resources{
ClusterLabels: map[string]string{"application": "spilo"},
ClusterNameLabel: clusterNameLabel,
DefaultCPURequest: "100m",
DefaultCPULimit: "1",
DefaultMemoryRequest: "100Mi",
DefaultMemoryLimit: "500Mi",
MinCPULimit: "250m",
MinMemoryLimit: "250Mi",
PodRoleLabel: roleLabel,
}
tests := []struct {
subTest string
config config.Config
pgSpec acidv1.Postgresql
expectedResources acidv1.Resources
}{
{
subTest: "test generation of default resources when empty in manifest",
config: config.Config{
Resources: configResources,
PodManagementPolicy: "ordered_ready",
SetMemoryRequestToLimit: false,
},
pgSpec: acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: clusterName,
Namespace: namespace,
},
Spec: acidv1.PostgresSpec{
TeamID: "acid",
Volume: acidv1.Volume{
Size: "1G",
},
},
},
expectedResources: acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "100m", Memory: "100Mi"},
ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "500Mi"},
},
},
{
subTest: "test generation of default resources for sidecar",
config: config.Config{
Resources: configResources,
PodManagementPolicy: "ordered_ready",
SetMemoryRequestToLimit: false,
},
pgSpec: acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: clusterName,
Namespace: namespace,
},
Spec: acidv1.PostgresSpec{
Sidecars: []acidv1.Sidecar{
acidv1.Sidecar{
Name: sidecarName,
},
},
TeamID: "acid",
Volume: acidv1.Volume{
Size: "1G",
},
},
},
expectedResources: acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "100m", Memory: "100Mi"},
ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "500Mi"},
},
},
{
subTest: "test generation of resources when only requests are defined in manifest",
config: config.Config{
Resources: configResources,
PodManagementPolicy: "ordered_ready",
SetMemoryRequestToLimit: false,
},
pgSpec: acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: clusterName,
Namespace: namespace,
},
Spec: acidv1.PostgresSpec{
Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "50m", Memory: "50Mi"},
},
TeamID: "acid",
Volume: acidv1.Volume{
Size: "1G",
},
},
},
expectedResources: acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "50m", Memory: "50Mi"},
ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "500Mi"},
},
},
{
subTest: "test generation of resources when only memory is defined in manifest",
config: config.Config{
Resources: configResources,
PodManagementPolicy: "ordered_ready",
SetMemoryRequestToLimit: false,
},
pgSpec: acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: clusterName,
Namespace: namespace,
},
Spec: acidv1.PostgresSpec{
Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{Memory: "100Mi"},
ResourceLimits: acidv1.ResourceDescription{Memory: "1Gi"},
},
TeamID: "acid",
Volume: acidv1.Volume{
Size: "1G",
},
},
},
expectedResources: acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "100m", Memory: "100Mi"},
ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "1Gi"},
},
},
{
subTest: "test SetMemoryRequestToLimit flag",
config: config.Config{
Resources: configResources,
PodManagementPolicy: "ordered_ready",
SetMemoryRequestToLimit: true,
},
pgSpec: acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: clusterName,
Namespace: namespace,
},
Spec: acidv1.PostgresSpec{
TeamID: "acid",
Volume: acidv1.Volume{
Size: "1G",
},
},
},
expectedResources: acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "100m", Memory: "500Mi"},
ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "500Mi"},
},
},
{
subTest: "test SetMemoryRequestToLimit flag for sidecar container, too",
config: config.Config{
Resources: configResources,
PodManagementPolicy: "ordered_ready",
SetMemoryRequestToLimit: true,
},
pgSpec: acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: clusterName,
Namespace: namespace,
},
Spec: acidv1.PostgresSpec{
Sidecars: []acidv1.Sidecar{
acidv1.Sidecar{
Name: sidecarName,
Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "10m", Memory: "10Mi"},
ResourceLimits: acidv1.ResourceDescription{CPU: "100m", Memory: "100Mi"},
},
},
},
TeamID: "acid",
Volume: acidv1.Volume{
Size: "1G",
},
},
},
expectedResources: acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "10m", Memory: "100Mi"},
ResourceLimits: acidv1.ResourceDescription{CPU: "100m", Memory: "100Mi"},
},
},
{
subTest: "test generating resources from manifest",
config: config.Config{
Resources: configResources,
PodManagementPolicy: "ordered_ready",
SetMemoryRequestToLimit: false,
},
pgSpec: acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: clusterName,
Namespace: namespace,
},
Spec: acidv1.PostgresSpec{
Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "10m", Memory: "250Mi"},
ResourceLimits: acidv1.ResourceDescription{CPU: "400m", Memory: "800Mi"},
},
TeamID: "acid",
Volume: acidv1.Volume{
Size: "1G",
},
},
},
expectedResources: acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "10m", Memory: "250Mi"},
ResourceLimits: acidv1.ResourceDescription{CPU: "400m", Memory: "800Mi"},
},
},
{
subTest: "test enforcing min cpu and memory limit",
config: config.Config{
Resources: configResources,
PodManagementPolicy: "ordered_ready",
SetMemoryRequestToLimit: false,
},
pgSpec: acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: clusterName,
Namespace: namespace,
},
Spec: acidv1.PostgresSpec{
Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "100m", Memory: "100Mi"},
ResourceLimits: acidv1.ResourceDescription{CPU: "200m", Memory: "200Mi"},
},
TeamID: "acid",
Volume: acidv1.Volume{
Size: "1G",
},
},
},
expectedResources: acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "100m", Memory: "100Mi"},
ResourceLimits: acidv1.ResourceDescription{CPU: "250m", Memory: "250Mi"},
},
},
{
subTest: "test min cpu and memory limit are not enforced on sidecar",
config: config.Config{
Resources: configResources,
PodManagementPolicy: "ordered_ready",
SetMemoryRequestToLimit: false,
},
pgSpec: acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: clusterName,
Namespace: namespace,
},
Spec: acidv1.PostgresSpec{
Sidecars: []acidv1.Sidecar{
acidv1.Sidecar{
Name: sidecarName,
Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "10m", Memory: "10Mi"},
ResourceLimits: acidv1.ResourceDescription{CPU: "100m", Memory: "100Mi"},
},
},
},
TeamID: "acid",
Volume: acidv1.Volume{
Size: "1G",
},
},
},
expectedResources: acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "10m", Memory: "10Mi"},
ResourceLimits: acidv1.ResourceDescription{CPU: "100m", Memory: "100Mi"},
},
},
}
for _, tt := range tests {
var cluster = New(
Config{
OpConfig: tt.config,
}, client, tt.pgSpec, logger, newEventRecorder)
cluster.Name = clusterName
cluster.Namespace = namespace
_, err := cluster.createStatefulSet()
if k8sutil.ResourceAlreadyExists(err) {
err = cluster.syncStatefulSet()
}
assert.NoError(t, err)
containers := cluster.Statefulset.Spec.Template.Spec.Containers
clusterResources, err := parseResourceRequirements(containers[0].Resources)
if len(containers) > 1 {
clusterResources, err = parseResourceRequirements(containers[1].Resources)
}
assert.NoError(t, err)
if !reflect.DeepEqual(tt.expectedResources, clusterResources) {
t.Errorf("%s - %s: expected %#v but got %#v", testName, tt.subTest, tt.expectedResources, clusterResources)
}
}
}
func TestGenerateCapabilities(t *testing.T) { func TestGenerateCapabilities(t *testing.T) {
testName := "TestGenerateCapabilities" testName := "TestGenerateCapabilities"
@@ -1614,7 +2111,7 @@ func TestVolumeSelector(t *testing.T) {
return acidv1.PostgresSpec{ return acidv1.PostgresSpec{
TeamID: "myapp", TeamID: "myapp",
NumberOfInstances: 0, NumberOfInstances: 0,
Resources: acidv1.Resources{ Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"}, ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"}, ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
}, },
+5 -5
View File
@@ -67,7 +67,7 @@ func (c *Cluster) markRollingUpdateFlagForPod(pod *v1.Pod, msg string) error {
return fmt.Errorf("could not form patch for pod's rolling update flag: %v", err) return fmt.Errorf("could not form patch for pod's rolling update flag: %v", err)
} }
err = retryutil.Retry(1*time.Second, 5*time.Second, err = retryutil.Retry(c.OpConfig.PatroniAPICheckInterval, c.OpConfig.PatroniAPICheckTimeout,
func() (bool, error) { func() (bool, error) {
_, err2 := c.KubeClient.Pods(pod.Namespace).Patch( _, err2 := c.KubeClient.Pods(pod.Namespace).Patch(
context.TODO(), context.TODO(),
@@ -356,7 +356,7 @@ func (c *Cluster) getPatroniConfig(pod *v1.Pod) (acidv1.Patroni, map[string]stri
pgParameters map[string]string pgParameters map[string]string
) )
podName := util.NameFromMeta(pod.ObjectMeta) podName := util.NameFromMeta(pod.ObjectMeta)
err := retryutil.Retry(1*time.Second, 5*time.Second, err := retryutil.Retry(c.OpConfig.PatroniAPICheckInterval, c.OpConfig.PatroniAPICheckTimeout,
func() (bool, error) { func() (bool, error) {
var err error var err error
patroniConfig, pgParameters, err = c.patroni.GetConfig(pod) patroniConfig, pgParameters, err = c.patroni.GetConfig(pod)
@@ -377,7 +377,7 @@ func (c *Cluster) getPatroniConfig(pod *v1.Pod) (acidv1.Patroni, map[string]stri
func (c *Cluster) getPatroniMemberData(pod *v1.Pod) (patroni.MemberData, error) { func (c *Cluster) getPatroniMemberData(pod *v1.Pod) (patroni.MemberData, error) {
var memberData patroni.MemberData var memberData patroni.MemberData
err := retryutil.Retry(1*time.Second, 5*time.Second, err := retryutil.Retry(c.OpConfig.PatroniAPICheckInterval, c.OpConfig.PatroniAPICheckTimeout,
func() (bool, error) { func() (bool, error) {
var err error var err error
memberData, err = c.patroni.GetMemberData(pod) memberData, err = c.patroni.GetMemberData(pod)
@@ -403,7 +403,7 @@ func (c *Cluster) recreatePod(podName spec.NamespacedName) (*v1.Pod, error) {
defer c.unregisterPodSubscriber(podName) defer c.unregisterPodSubscriber(podName)
stopChan := make(chan struct{}) stopChan := make(chan struct{})
err := retryutil.Retry(1*time.Second, 5*time.Second, err := retryutil.Retry(c.OpConfig.PatroniAPICheckInterval, c.OpConfig.PatroniAPICheckTimeout,
func() (bool, error) { func() (bool, error) {
err2 := c.KubeClient.Pods(podName.Namespace).Delete( err2 := c.KubeClient.Pods(podName.Namespace).Delete(
context.TODO(), context.TODO(),
@@ -492,7 +492,7 @@ func (c *Cluster) getSwitchoverCandidate(master *v1.Pod) (spec.NamespacedName, e
candidates := make([]patroni.ClusterMember, 0) candidates := make([]patroni.ClusterMember, 0)
syncCandidates := make([]patroni.ClusterMember, 0) syncCandidates := make([]patroni.ClusterMember, 0)
err := retryutil.Retry(1*time.Second, 5*time.Second, err := retryutil.Retry(c.OpConfig.PatroniAPICheckInterval, c.OpConfig.PatroniAPICheckTimeout,
func() (bool, error) { func() (bool, error) {
var err error var err error
members, err = c.patroni.GetClusterMembers(master) members, err = c.patroni.GetClusterMembers(master)
+9 -1
View File
@@ -6,11 +6,13 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"testing" "testing"
"time"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/zalando/postgres-operator/mocks" "github.com/zalando/postgres-operator/mocks"
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
"github.com/zalando/postgres-operator/pkg/spec" "github.com/zalando/postgres-operator/pkg/spec"
"github.com/zalando/postgres-operator/pkg/util/config"
"github.com/zalando/postgres-operator/pkg/util/k8sutil" "github.com/zalando/postgres-operator/pkg/util/k8sutil"
"github.com/zalando/postgres-operator/pkg/util/patroni" "github.com/zalando/postgres-operator/pkg/util/patroni"
) )
@@ -22,7 +24,13 @@ func TestGetSwitchoverCandidate(t *testing.T) {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
defer ctrl.Finish() defer ctrl.Finish()
var cluster = New(Config{}, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder) var cluster = New(
Config{
OpConfig: config.Config{
PatroniAPICheckInterval: time.Duration(1),
PatroniAPICheckTimeout: time.Duration(5),
},
}, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder)
// simulate different member scenarios // simulate different member scenarios
tests := []struct { tests := []struct {
+11 -16
View File
@@ -275,7 +275,7 @@ func (c *Cluster) createService(role PostgresRole) (*v1.Service, error) {
return service, nil return service, nil
} }
func (c *Cluster) updateService(role PostgresRole, newService *v1.Service) error { func (c *Cluster) updateService(role PostgresRole, oldService *v1.Service, newService *v1.Service) (*v1.Service, error) {
var ( var (
svc *v1.Service svc *v1.Service
err error err error
@@ -283,11 +283,7 @@ func (c *Cluster) updateService(role PostgresRole, newService *v1.Service) error
c.setProcessName("updating %v service", role) c.setProcessName("updating %v service", role)
if c.Services[role] == nil { serviceName := util.NameFromMeta(oldService.ObjectMeta)
return fmt.Errorf("there is no service in the cluster")
}
serviceName := util.NameFromMeta(c.Services[role].ObjectMeta)
// update the service annotation in order to propagate ELB notation. // update the service annotation in order to propagate ELB notation.
if len(newService.ObjectMeta.Annotations) > 0 { if len(newService.ObjectMeta.Annotations) > 0 {
@@ -301,39 +297,38 @@ func (c *Cluster) updateService(role PostgresRole, newService *v1.Service) error
"") "")
if err != nil { if err != nil {
return fmt.Errorf("could not replace annotations for the service %q: %v", serviceName, err) return nil, fmt.Errorf("could not replace annotations for the service %q: %v", serviceName, err)
} }
} else { } else {
return fmt.Errorf("could not form patch for the service metadata: %v", err) return nil, fmt.Errorf("could not form patch for the service metadata: %v", err)
} }
} }
// now, patch the service spec, but when disabling LoadBalancers do update instead // now, patch the service spec, but when disabling LoadBalancers do update instead
// patch does not work because of LoadBalancerSourceRanges field (even if set to nil) // patch does not work because of LoadBalancerSourceRanges field (even if set to nil)
oldServiceType := c.Services[role].Spec.Type oldServiceType := oldService.Spec.Type
newServiceType := newService.Spec.Type newServiceType := newService.Spec.Type
if newServiceType == "ClusterIP" && newServiceType != oldServiceType { if newServiceType == "ClusterIP" && newServiceType != oldServiceType {
newService.ResourceVersion = c.Services[role].ResourceVersion newService.ResourceVersion = oldService.ResourceVersion
newService.Spec.ClusterIP = c.Services[role].Spec.ClusterIP newService.Spec.ClusterIP = oldService.Spec.ClusterIP
svc, err = c.KubeClient.Services(serviceName.Namespace).Update(context.TODO(), newService, metav1.UpdateOptions{}) svc, err = c.KubeClient.Services(serviceName.Namespace).Update(context.TODO(), newService, metav1.UpdateOptions{})
if err != nil { if err != nil {
return fmt.Errorf("could not update service %q: %v", serviceName, err) return nil, fmt.Errorf("could not update service %q: %v", serviceName, err)
} }
} else { } else {
patchData, err := specPatch(newService.Spec) patchData, err := specPatch(newService.Spec)
if err != nil { if err != nil {
return fmt.Errorf("could not form patch for the service %q: %v", serviceName, err) return nil, fmt.Errorf("could not form patch for the service %q: %v", serviceName, err)
} }
svc, err = c.KubeClient.Services(serviceName.Namespace).Patch( svc, err = c.KubeClient.Services(serviceName.Namespace).Patch(
context.TODO(), serviceName.Name, types.MergePatchType, patchData, metav1.PatchOptions{}, "") context.TODO(), serviceName.Name, types.MergePatchType, patchData, metav1.PatchOptions{}, "")
if err != nil { if err != nil {
return fmt.Errorf("could not patch service %q: %v", serviceName, err) return nil, fmt.Errorf("could not patch service %q: %v", serviceName, err)
} }
} }
c.Services[role] = svc
return nil return svc, nil
} }
func (c *Cluster) deleteService(role PostgresRole) error { func (c *Cluster) deleteService(role PostgresRole) error {
+29 -5
View File
@@ -189,7 +189,7 @@ func (c *Cluster) generateFabricEventStream(appId string) *zalandov1.FabricEvent
} }
} }
func (c *Cluster) getEventStreamSource(stream acidv1.Stream, tableName, idColumn string) zalandov1.EventStreamSource { func (c *Cluster) getEventStreamSource(stream acidv1.Stream, tableName string, idColumn *string) zalandov1.EventStreamSource {
table, schema := getTableSchema(tableName) table, schema := getTableSchema(tableName)
streamFilter := stream.Filter[tableName] streamFilter := stream.Filter[tableName]
return zalandov1.EventStreamSource{ return zalandov1.EventStreamSource{
@@ -204,7 +204,7 @@ func (c *Cluster) getEventStreamSource(stream acidv1.Stream, tableName, idColumn
} }
} }
func getEventStreamFlow(stream acidv1.Stream, payloadColumn string) zalandov1.EventStreamFlow { func getEventStreamFlow(stream acidv1.Stream, payloadColumn *string) zalandov1.EventStreamFlow {
return zalandov1.EventStreamFlow{ return zalandov1.EventStreamFlow{
Type: constants.EventStreamFlowPgGenericType, Type: constants.EventStreamFlowPgGenericType,
PayloadColumn: payloadColumn, PayloadColumn: payloadColumn,
@@ -230,7 +230,7 @@ func getTableSchema(fullTableName string) (tableName, schemaName string) {
return tableName, schemaName return tableName, schemaName
} }
func getOutboxTable(tableName, idColumn string) zalandov1.EventStreamTable { func getOutboxTable(tableName string, idColumn *string) zalandov1.EventStreamTable {
return zalandov1.EventStreamTable{ return zalandov1.EventStreamTable{
Name: tableName, Name: tableName,
IDColumn: idColumn, IDColumn: idColumn,
@@ -347,8 +347,8 @@ func (c *Cluster) createOrUpdateStreams() error {
c.logger.Infof("event stream %q has been successfully created", fesName) c.logger.Infof("event stream %q has been successfully created", fesName)
} else { } else {
desiredStreams := c.generateFabricEventStream(appId) desiredStreams := c.generateFabricEventStream(appId)
if !reflect.DeepEqual(effectiveStreams.Spec, desiredStreams.Spec) { if match, reason := sameStreams(effectiveStreams.Spec.EventStreams, desiredStreams.Spec.EventStreams); !match {
c.logger.Debug("updating event streams") c.logger.Debugf("updating event streams: %s", reason)
desiredStreams.ObjectMeta.ResourceVersion = effectiveStreams.ObjectMeta.ResourceVersion desiredStreams.ObjectMeta.ResourceVersion = effectiveStreams.ObjectMeta.ResourceVersion
err = c.updateStreams(desiredStreams) err = c.updateStreams(desiredStreams)
if err != nil { if err != nil {
@@ -361,3 +361,27 @@ func (c *Cluster) createOrUpdateStreams() error {
return nil return nil
} }
func sameStreams(curEventStreams, newEventStreams []zalandov1.EventStream) (match bool, reason string) {
if len(newEventStreams) != len(curEventStreams) {
return false, "number of defined streams is different"
}
for _, newStream := range newEventStreams {
match = false
reason = "event stream specs differ"
for _, curStream := range curEventStreams {
if reflect.DeepEqual(newStream.EventStreamSource, curStream.EventStreamSource) &&
reflect.DeepEqual(newStream.EventStreamFlow, curStream.EventStreamFlow) &&
reflect.DeepEqual(newStream.EventStreamSink, curStream.EventStreamSink) {
match = true
break
}
}
if !match {
return false, reason
}
}
return true, ""
}
+155 -31
View File
@@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
v1 "github.com/zalando/postgres-operator/pkg/apis/zalando.org/v1" zalandov1 "github.com/zalando/postgres-operator/pkg/apis/zalando.org/v1"
fakezalandov1 "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/fake" fakezalandov1 "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/fake"
"github.com/zalando/postgres-operator/pkg/util" "github.com/zalando/postgres-operator/pkg/util"
"github.com/zalando/postgres-operator/pkg/util/config" "github.com/zalando/postgres-operator/pkg/util/config"
@@ -63,15 +63,18 @@ var (
Database: "foo", Database: "foo",
Tables: map[string]acidv1.StreamTable{ Tables: map[string]acidv1.StreamTable{
"data.bar": acidv1.StreamTable{ "data.bar": acidv1.StreamTable{
EventType: "stream_type_a", EventType: "stream-type-a",
IdColumn: "b_id", IdColumn: k8sutil.StringToPointer("b_id"),
PayloadColumn: "b_payload", PayloadColumn: k8sutil.StringToPointer("b_payload"),
},
"data.foobar": acidv1.StreamTable{
EventType: "stream-type-b",
}, },
}, },
Filter: map[string]string{ Filter: map[string]*string{
"data.bar": "[?(@.source.txId > 500 && @.source.lsn > 123456)]", "data.bar": k8sutil.StringToPointer("[?(@.source.txId > 500 && @.source.lsn > 123456)]"),
}, },
BatchSize: uint32(100), BatchSize: k8sutil.UInt32ToPointer(uint32(100)),
}, },
}, },
Volume: acidv1.Volume{ Volume: acidv1.Volume{
@@ -80,7 +83,7 @@ var (
}, },
} }
fes = &v1.FabricEventStream{ fes = &zalandov1.FabricEventStream{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
APIVersion: constants.EventStreamCRDApiVersion, APIVersion: constants.EventStreamCRDApiVersion,
Kind: constants.EventStreamCRDKind, Kind: constants.EventStreamCRDKind,
@@ -97,23 +100,23 @@ var (
}, },
}, },
}, },
Spec: v1.FabricEventStreamSpec{ Spec: zalandov1.FabricEventStreamSpec{
ApplicationId: appId, ApplicationId: appId,
EventStreams: []v1.EventStream{ EventStreams: []zalandov1.EventStream{
{ zalandov1.EventStream{
EventStreamFlow: v1.EventStreamFlow{ EventStreamFlow: zalandov1.EventStreamFlow{
PayloadColumn: "b_payload", PayloadColumn: k8sutil.StringToPointer("b_payload"),
Type: constants.EventStreamFlowPgGenericType, Type: constants.EventStreamFlowPgGenericType,
}, },
EventStreamSink: v1.EventStreamSink{ EventStreamSink: zalandov1.EventStreamSink{
EventType: "stream_type_a", EventType: "stream-type-a",
MaxBatchSize: uint32(100), MaxBatchSize: k8sutil.UInt32ToPointer(uint32(100)),
Type: constants.EventStreamSinkNakadiType, Type: constants.EventStreamSinkNakadiType,
}, },
EventStreamSource: v1.EventStreamSource{ EventStreamSource: zalandov1.EventStreamSource{
Filter: "[?(@.source.txId > 500 && @.source.lsn > 123456)]", Filter: k8sutil.StringToPointer("[?(@.source.txId > 500 && @.source.lsn > 123456)]"),
Connection: v1.Connection{ Connection: zalandov1.Connection{
DBAuth: v1.DBAuth{ DBAuth: zalandov1.DBAuth{
Name: fmt.Sprintf("fes-user.%s.credentials.postgresql.acid.zalan.do", clusterName), Name: fmt.Sprintf("fes-user.%s.credentials.postgresql.acid.zalan.do", clusterName),
PasswordKey: "password", PasswordKey: "password",
Type: constants.EventStreamSourceAuthType, Type: constants.EventStreamSourceAuthType,
@@ -124,13 +127,41 @@ var (
PluginType: constants.EventStreamSourcePluginType, PluginType: constants.EventStreamSourcePluginType,
}, },
Schema: "data", Schema: "data",
EventStreamTable: v1.EventStreamTable{ EventStreamTable: zalandov1.EventStreamTable{
IDColumn: "b_id", IDColumn: k8sutil.StringToPointer("b_id"),
Name: "bar", Name: "bar",
}, },
Type: constants.EventStreamSourcePGType, Type: constants.EventStreamSourcePGType,
}, },
}, },
zalandov1.EventStream{
EventStreamFlow: zalandov1.EventStreamFlow{
Type: constants.EventStreamFlowPgGenericType,
},
EventStreamSink: zalandov1.EventStreamSink{
EventType: "stream-type-b",
MaxBatchSize: k8sutil.UInt32ToPointer(uint32(100)),
Type: constants.EventStreamSinkNakadiType,
},
EventStreamSource: zalandov1.EventStreamSource{
Connection: zalandov1.Connection{
DBAuth: zalandov1.DBAuth{
Name: fmt.Sprintf("fes-user.%s.credentials.postgresql.acid.zalan.do", clusterName),
PasswordKey: "password",
Type: constants.EventStreamSourceAuthType,
UserKey: "username",
},
Url: fmt.Sprintf("jdbc:postgresql://%s.%s/foo?user=%s&ssl=true&sslmode=require", clusterName, namespace, fesUser),
SlotName: slotName,
PluginType: constants.EventStreamSourcePluginType,
},
Schema: "data",
EventStreamTable: zalandov1.EventStreamTable{
Name: "foobar",
},
Type: constants.EventStreamSourcePGType,
},
},
}, },
}, },
} }
@@ -161,23 +192,116 @@ func TestGenerateFabricEventStream(t *testing.T) {
cluster.Name = clusterName cluster.Name = clusterName
cluster.Namespace = namespace cluster.Namespace = namespace
// create statefulset to have ownerReference for streams
_, err := cluster.createStatefulSet() _, err := cluster.createStatefulSet()
assert.NoError(t, err) assert.NoError(t, err)
// create the streams
err = cluster.createOrUpdateStreams() err = cluster.createOrUpdateStreams()
assert.NoError(t, err) assert.NoError(t, err)
// compare generated stream with expected stream
result := cluster.generateFabricEventStream(appId) result := cluster.generateFabricEventStream(appId)
if match, _ := sameStreams(result.Spec.EventStreams, fes.Spec.EventStreams); !match {
if !reflect.DeepEqual(result, fes) { t.Errorf("malformed FabricEventStream, expected %#v, got %#v", fes, result)
t.Errorf("Malformed FabricEventStream, expected %#v, got %#v", fes, result)
} }
// compare stream resturned from API with expected stream
streamCRD, err := cluster.KubeClient.FabricEventStreams(namespace).Get(context.TODO(), fesName, metav1.GetOptions{}) streamCRD, err := cluster.KubeClient.FabricEventStreams(namespace).Get(context.TODO(), fesName, metav1.GetOptions{})
assert.NoError(t, err) assert.NoError(t, err)
if match, _ := sameStreams(streamCRD.Spec.EventStreams, fes.Spec.EventStreams); !match {
t.Errorf("malformed FabricEventStream returned from API, expected %#v, got %#v", fes, streamCRD)
}
if !reflect.DeepEqual(streamCRD, fes) { // sync streams once again
t.Errorf("Malformed FabricEventStream, expected %#v, got %#v", fes, streamCRD) err = cluster.createOrUpdateStreams()
assert.NoError(t, err)
// compare stream resturned from API with generated stream
streamCRD, err = cluster.KubeClient.FabricEventStreams(namespace).Get(context.TODO(), fesName, metav1.GetOptions{})
assert.NoError(t, err)
if match, _ := sameStreams(streamCRD.Spec.EventStreams, result.Spec.EventStreams); !match {
t.Errorf("returned FabricEventStream differs from generated one, expected %#v, got %#v", result, streamCRD)
}
}
func TestSameStreams(t *testing.T) {
testName := "TestSameStreams"
stream1 := zalandov1.EventStream{
EventStreamFlow: zalandov1.EventStreamFlow{},
EventStreamSink: zalandov1.EventStreamSink{
EventType: "stream-type-a",
},
EventStreamSource: zalandov1.EventStreamSource{
EventStreamTable: zalandov1.EventStreamTable{
Name: "foo",
},
},
}
stream2 := zalandov1.EventStream{
EventStreamFlow: zalandov1.EventStreamFlow{},
EventStreamSink: zalandov1.EventStreamSink{
EventType: "stream-type-b",
},
EventStreamSource: zalandov1.EventStreamSource{
EventStreamTable: zalandov1.EventStreamTable{
Name: "bar",
},
},
}
tests := []struct {
subTest string
streamsA []zalandov1.EventStream
streamsB []zalandov1.EventStream
match bool
reason string
}{
{
subTest: "identical streams",
streamsA: []zalandov1.EventStream{stream1, stream2},
streamsB: []zalandov1.EventStream{stream1, stream2},
match: true,
reason: "",
},
{
subTest: "same streams different order",
streamsA: []zalandov1.EventStream{stream1, stream2},
streamsB: []zalandov1.EventStream{stream2, stream1},
match: true,
reason: "",
},
{
subTest: "same streams different order",
streamsA: []zalandov1.EventStream{stream1},
streamsB: []zalandov1.EventStream{stream1, stream2},
match: false,
reason: "number of defined streams is different",
},
{
subTest: "different number of streams",
streamsA: []zalandov1.EventStream{stream1},
streamsB: []zalandov1.EventStream{stream1, stream2},
match: false,
reason: "number of defined streams is different",
},
{
subTest: "event stream specs differ",
streamsA: []zalandov1.EventStream{stream1, stream2},
streamsB: fes.Spec.EventStreams,
match: false,
reason: "number of defined streams is different",
},
}
for _, tt := range tests {
streamsMatch, matchReason := sameStreams(tt.streamsA, tt.streamsB)
if streamsMatch != tt.match {
t.Errorf("%s %s: unexpected match result when comparing streams: got %s, epxected %s",
testName, tt.subTest, matchReason, tt.reason)
}
} }
} }
@@ -213,12 +337,12 @@ func TestUpdateFabricEventStream(t *testing.T) {
Database: dbName, Database: dbName,
Tables: map[string]acidv1.StreamTable{ Tables: map[string]acidv1.StreamTable{
"data.bar": acidv1.StreamTable{ "data.bar": acidv1.StreamTable{
EventType: "stream_type_b", EventType: "stream-type-c",
IdColumn: "b_id", IdColumn: k8sutil.StringToPointer("b_id"),
PayloadColumn: "b_payload", PayloadColumn: k8sutil.StringToPointer("b_payload"),
}, },
}, },
BatchSize: uint32(250), BatchSize: k8sutil.UInt32ToPointer(uint32(250)),
}, },
} }
patch, err := json.Marshal(struct { patch, err := json.Marshal(struct {
+4 -7
View File
@@ -76,11 +76,6 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error {
} }
} }
if err = c.enforceMinResourceLimits(&c.Spec); err != nil {
err = fmt.Errorf("could not enforce minimum resource limits: %v", err)
return err
}
c.logger.Debug("syncing statefulsets") c.logger.Debug("syncing statefulsets")
if err = c.syncStatefulSet(); err != nil { if err = c.syncStatefulSet(); err != nil {
if !k8sutil.ResourceAlreadyExists(err) { if !k8sutil.ResourceAlreadyExists(err) {
@@ -172,11 +167,13 @@ func (c *Cluster) syncService(role PostgresRole) error {
if svc, err = c.KubeClient.Services(c.Namespace).Get(context.TODO(), c.serviceName(role), metav1.GetOptions{}); err == nil { if svc, err = c.KubeClient.Services(c.Namespace).Get(context.TODO(), c.serviceName(role), metav1.GetOptions{}); err == nil {
c.Services[role] = svc c.Services[role] = svc
desiredSvc := c.generateService(role, &c.Spec) desiredSvc := c.generateService(role, &c.Spec)
if match, reason := k8sutil.SameService(svc, desiredSvc); !match { if match, reason := c.compareServices(svc, desiredSvc); !match {
c.logServiceChanges(role, svc, desiredSvc, false, reason) c.logServiceChanges(role, svc, desiredSvc, false, reason)
if err = c.updateService(role, desiredSvc); err != nil { updatedSvc, err := c.updateService(role, svc, desiredSvc)
if err != nil {
return fmt.Errorf("could not update %s service to match desired state: %v", role, err) return fmt.Errorf("could not update %s service to match desired state: %v", role, err)
} }
c.Services[role] = updatedSvc
c.logger.Infof("%s service %q is in the desired state now", role, util.NameFromMeta(desiredSvc.ObjectMeta)) c.logger.Infof("%s service %q is in the desired state now", role, util.NameFromMeta(desiredSvc.ObjectMeta))
} }
return nil return nil
+12
View File
@@ -594,3 +594,15 @@ func trimCronjobName(name string) string {
} }
return name return name
} }
func parseResourceRequirements(resourcesRequirement v1.ResourceRequirements) (acidv1.Resources, error) {
var resources acidv1.Resources
resourcesJSON, err := json.Marshal(resourcesRequirement)
if err != nil {
return acidv1.Resources{}, fmt.Errorf("could not marshal K8s resources requirements")
}
if err = json.Unmarshal(resourcesJSON, &resources); err != nil {
return acidv1.Resources{}, fmt.Errorf("could not unmarshal K8s resources requirements into acidv1.Resources struct")
}
return resources, nil
}
+1 -1
View File
@@ -57,7 +57,7 @@ func TestInheritedAnnotations(t *testing.T) {
ConnectionPoolerDefaultCPULimit: "100m", ConnectionPoolerDefaultCPULimit: "100m",
ConnectionPoolerDefaultMemoryRequest: "100Mi", ConnectionPoolerDefaultMemoryRequest: "100Mi",
ConnectionPoolerDefaultMemoryLimit: "100Mi", ConnectionPoolerDefaultMemoryLimit: "100Mi",
NumberOfInstances: int32ToPointer(1), NumberOfInstances: k8sutil.Int32ToPointer(1),
}, },
PodManagementPolicy: "ordered_ready", PodManagementPolicy: "ordered_ready",
Resources: config.Resources{ Resources: config.Resources{
+13 -9
View File
@@ -10,6 +10,7 @@ import (
"github.com/zalando/postgres-operator/pkg/util" "github.com/zalando/postgres-operator/pkg/util"
"github.com/zalando/postgres-operator/pkg/util/config" "github.com/zalando/postgres-operator/pkg/util/config"
"github.com/zalando/postgres-operator/pkg/util/constants" "github.com/zalando/postgres-operator/pkg/util/constants"
"github.com/zalando/postgres-operator/pkg/util/k8sutil"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
@@ -24,10 +25,6 @@ func (c *Controller) readOperatorConfigurationFromCRD(configObjectNamespace, con
return config, nil return config, nil
} }
func int32ToPointer(value int32) *int32 {
return &value
}
// importConfigurationFromCRD is a transitional function that converts CRD configuration to the one based on the configmap // importConfigurationFromCRD is a transitional function that converts CRD configuration to the one based on the configmap
func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigurationData) *config.Config { func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigurationData) *config.Config {
result := &config.Config{} result := &config.Config{}
@@ -55,6 +52,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
// user config // user config
result.SuperUsername = util.Coalesce(fromCRD.PostgresUsersConfiguration.SuperUsername, "postgres") result.SuperUsername = util.Coalesce(fromCRD.PostgresUsersConfiguration.SuperUsername, "postgres")
result.ReplicationUsername = util.Coalesce(fromCRD.PostgresUsersConfiguration.ReplicationUsername, "standby") result.ReplicationUsername = util.Coalesce(fromCRD.PostgresUsersConfiguration.ReplicationUsername, "standby")
result.AdditionalOwnerRoles = fromCRD.PostgresUsersConfiguration.AdditionalOwnerRoles
result.EnablePasswordRotation = fromCRD.PostgresUsersConfiguration.EnablePasswordRotation result.EnablePasswordRotation = fromCRD.PostgresUsersConfiguration.EnablePasswordRotation
result.PasswordRotationInterval = util.CoalesceUInt32(fromCRD.PostgresUsersConfiguration.PasswordRotationInterval, 90) result.PasswordRotationInterval = util.CoalesceUInt32(fromCRD.PostgresUsersConfiguration.PasswordRotationInterval, 90)
result.PasswordRotationUserRetention = util.CoalesceUInt32(fromCRD.PostgresUsersConfiguration.DeepCopy().PasswordRotationUserRetention, 180) result.PasswordRotationUserRetention = util.CoalesceUInt32(fromCRD.PostgresUsersConfiguration.DeepCopy().PasswordRotationUserRetention, 180)
@@ -110,6 +108,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.InheritedLabels = fromCRD.Kubernetes.InheritedLabels result.InheritedLabels = fromCRD.Kubernetes.InheritedLabels
result.InheritedAnnotations = fromCRD.Kubernetes.InheritedAnnotations result.InheritedAnnotations = fromCRD.Kubernetes.InheritedAnnotations
result.DownscalerAnnotations = fromCRD.Kubernetes.DownscalerAnnotations result.DownscalerAnnotations = fromCRD.Kubernetes.DownscalerAnnotations
result.IgnoredAnnotations = fromCRD.Kubernetes.IgnoredAnnotations
result.ClusterNameLabel = util.Coalesce(fromCRD.Kubernetes.ClusterNameLabel, "cluster-name") result.ClusterNameLabel = util.Coalesce(fromCRD.Kubernetes.ClusterNameLabel, "cluster-name")
result.DeleteAnnotationDateKey = fromCRD.Kubernetes.DeleteAnnotationDateKey result.DeleteAnnotationDateKey = fromCRD.Kubernetes.DeleteAnnotationDateKey
result.DeleteAnnotationNameKey = fromCRD.Kubernetes.DeleteAnnotationNameKey result.DeleteAnnotationNameKey = fromCRD.Kubernetes.DeleteAnnotationNameKey
@@ -137,11 +136,15 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.PodDeletionWaitTimeout = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.PodDeletionWaitTimeout), "10m") result.PodDeletionWaitTimeout = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.PodDeletionWaitTimeout), "10m")
result.ReadyWaitInterval = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.ReadyWaitInterval), "4s") result.ReadyWaitInterval = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.ReadyWaitInterval), "4s")
result.ReadyWaitTimeout = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.ReadyWaitTimeout), "30s") result.ReadyWaitTimeout = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.ReadyWaitTimeout), "30s")
result.PatroniAPICheckInterval = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.PatroniAPICheckInterval), "1s")
result.PatroniAPICheckTimeout = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.PatroniAPICheckTimeout), "5s")
// load balancer config // load balancer config
result.DbHostedZone = util.Coalesce(fromCRD.LoadBalancer.DbHostedZone, "db.example.com") result.DbHostedZone = util.Coalesce(fromCRD.LoadBalancer.DbHostedZone, "db.example.com")
result.EnableMasterLoadBalancer = fromCRD.LoadBalancer.EnableMasterLoadBalancer result.EnableMasterLoadBalancer = fromCRD.LoadBalancer.EnableMasterLoadBalancer
result.EnableMasterPoolerLoadBalancer = fromCRD.LoadBalancer.EnableMasterPoolerLoadBalancer
result.EnableReplicaLoadBalancer = fromCRD.LoadBalancer.EnableReplicaLoadBalancer result.EnableReplicaLoadBalancer = fromCRD.LoadBalancer.EnableReplicaLoadBalancer
result.EnableReplicaPoolerLoadBalancer = fromCRD.LoadBalancer.EnableReplicaPoolerLoadBalancer
result.CustomServiceAnnotations = fromCRD.LoadBalancer.CustomServiceAnnotations result.CustomServiceAnnotations = fromCRD.LoadBalancer.CustomServiceAnnotations
result.MasterDNSNameFormat = fromCRD.LoadBalancer.MasterDNSNameFormat result.MasterDNSNameFormat = fromCRD.LoadBalancer.MasterDNSNameFormat
result.ReplicaDNSNameFormat = fromCRD.LoadBalancer.ReplicaDNSNameFormat result.ReplicaDNSNameFormat = fromCRD.LoadBalancer.ReplicaDNSNameFormat
@@ -170,6 +173,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.LogicalBackupS3AccessKeyID = fromCRD.LogicalBackup.S3AccessKeyID result.LogicalBackupS3AccessKeyID = fromCRD.LogicalBackup.S3AccessKeyID
result.LogicalBackupS3SecretAccessKey = fromCRD.LogicalBackup.S3SecretAccessKey result.LogicalBackupS3SecretAccessKey = fromCRD.LogicalBackup.S3SecretAccessKey
result.LogicalBackupS3SSE = fromCRD.LogicalBackup.S3SSE result.LogicalBackupS3SSE = fromCRD.LogicalBackup.S3SSE
result.LogicalBackupS3RetentionTime = fromCRD.LogicalBackup.RetentionTime
result.LogicalBackupGoogleApplicationCredentials = fromCRD.LogicalBackup.GoogleApplicationCredentials result.LogicalBackupGoogleApplicationCredentials = fromCRD.LogicalBackup.GoogleApplicationCredentials
result.LogicalBackupJobPrefix = util.Coalesce(fromCRD.LogicalBackup.JobPrefix, "logical-backup-") result.LogicalBackupJobPrefix = util.Coalesce(fromCRD.LogicalBackup.JobPrefix, "logical-backup-")
@@ -186,7 +190,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.TeamAdminRole = fromCRD.TeamsAPI.TeamAdminRole result.TeamAdminRole = fromCRD.TeamsAPI.TeamAdminRole
result.PamRoleName = util.Coalesce(fromCRD.TeamsAPI.PamRoleName, "zalandos") result.PamRoleName = util.Coalesce(fromCRD.TeamsAPI.PamRoleName, "zalandos")
result.PamConfiguration = util.Coalesce(fromCRD.TeamsAPI.PamConfiguration, "https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees") result.PamConfiguration = util.Coalesce(fromCRD.TeamsAPI.PamConfiguration, "https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees")
result.ProtectedRoles = util.CoalesceStrArr(fromCRD.TeamsAPI.ProtectedRoles, []string{"admin"}) result.ProtectedRoles = util.CoalesceStrArr(fromCRD.TeamsAPI.ProtectedRoles, []string{"admin", "cron_admin"})
result.PostgresSuperuserTeams = fromCRD.TeamsAPI.PostgresSuperuserTeams result.PostgresSuperuserTeams = fromCRD.TeamsAPI.PostgresSuperuserTeams
result.EnablePostgresTeamCRD = fromCRD.TeamsAPI.EnablePostgresTeamCRD result.EnablePostgresTeamCRD = fromCRD.TeamsAPI.EnablePostgresTeamCRD
result.EnablePostgresTeamCRDSuperusers = fromCRD.TeamsAPI.EnablePostgresTeamCRDSuperusers result.EnablePostgresTeamCRDSuperusers = fromCRD.TeamsAPI.EnablePostgresTeamCRDSuperusers
@@ -211,11 +215,11 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
// so ensure default values here. // so ensure default values here.
result.ConnectionPooler.NumberOfInstances = util.CoalesceInt32( result.ConnectionPooler.NumberOfInstances = util.CoalesceInt32(
fromCRD.ConnectionPooler.NumberOfInstances, fromCRD.ConnectionPooler.NumberOfInstances,
int32ToPointer(2)) k8sutil.Int32ToPointer(2))
result.ConnectionPooler.NumberOfInstances = util.MaxInt32( result.ConnectionPooler.NumberOfInstances = util.MaxInt32(
result.ConnectionPooler.NumberOfInstances, result.ConnectionPooler.NumberOfInstances,
int32ToPointer(2)) k8sutil.Int32ToPointer(2))
result.ConnectionPooler.Schema = util.Coalesce( result.ConnectionPooler.Schema = util.Coalesce(
fromCRD.ConnectionPooler.Schema, fromCRD.ConnectionPooler.Schema,
@@ -226,7 +230,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
constants.ConnectionPoolerUserName) constants.ConnectionPoolerUserName)
if result.ConnectionPooler.User == result.SuperUsername { if result.ConnectionPooler.User == result.SuperUsername {
msg := "Connection pool user is not allowed to be the same as super user, username: %s" msg := "connection pool user is not allowed to be the same as super user, username: %s"
panic(fmt.Errorf(msg, result.ConnectionPooler.User)) panic(fmt.Errorf(msg, result.ConnectionPooler.User))
} }
@@ -256,7 +260,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.ConnectionPooler.MaxDBConnections = util.CoalesceInt32( result.ConnectionPooler.MaxDBConnections = util.CoalesceInt32(
fromCRD.ConnectionPooler.MaxDBConnections, fromCRD.ConnectionPooler.MaxDBConnections,
int32ToPointer(constants.ConnectionPoolerMaxDBConnections)) k8sutil.Int32ToPointer(constants.ConnectionPoolerMaxDBConnections))
return result return result
} }
+2
View File
@@ -30,6 +30,7 @@ const (
RoleOriginManifest RoleOriginManifest
RoleOriginInfrastructure RoleOriginInfrastructure
RoleOriginTeamsAPI RoleOriginTeamsAPI
RoleOriginSpilo
RoleOriginSystem RoleOriginSystem
RoleOriginBootstrap RoleOriginBootstrap
RoleConnectionPooler RoleConnectionPooler
@@ -118,6 +119,7 @@ type ControllerConfig struct {
CRDReadyWaitTimeout time.Duration CRDReadyWaitTimeout time.Duration
ConfigMapName NamespacedName ConfigMapName NamespacedName
Namespace string Namespace string
IgnoredAnnotations []string
EnableJsonLogging bool EnableJsonLogging bool
} }
+9 -2
View File
@@ -42,6 +42,7 @@ type Resources struct {
InheritedLabels []string `name:"inherited_labels" default:""` InheritedLabels []string `name:"inherited_labels" default:""`
InheritedAnnotations []string `name:"inherited_annotations" default:""` InheritedAnnotations []string `name:"inherited_annotations" default:""`
DownscalerAnnotations []string `name:"downscaler_annotations"` DownscalerAnnotations []string `name:"downscaler_annotations"`
IgnoredAnnotations []string `name:"ignored_annotations"`
ClusterNameLabel string `name:"cluster_name_label" default:"cluster-name"` ClusterNameLabel string `name:"cluster_name_label" default:"cluster-name"`
DeleteAnnotationDateKey string `name:"delete_annotation_date_key"` DeleteAnnotationDateKey string `name:"delete_annotation_date_key"`
DeleteAnnotationNameKey string `name:"delete_annotation_name_key"` DeleteAnnotationNameKey string `name:"delete_annotation_name_key"`
@@ -101,6 +102,7 @@ type Auth struct {
InfrastructureRolesDefs string `name:"infrastructure_roles_secrets"` InfrastructureRolesDefs string `name:"infrastructure_roles_secrets"`
SuperUsername string `name:"super_username" default:"postgres"` SuperUsername string `name:"super_username" default:"postgres"`
ReplicationUsername string `name:"replication_username" default:"standby"` ReplicationUsername string `name:"replication_username" default:"standby"`
AdditionalOwnerRoles []string `name:"additional_owner_roles" default:""`
EnablePasswordRotation bool `name:"enable_password_rotation" default:"false"` EnablePasswordRotation bool `name:"enable_password_rotation" default:"false"`
PasswordRotationInterval uint32 `name:"password_rotation_interval" default:"90"` PasswordRotationInterval uint32 `name:"password_rotation_interval" default:"90"`
PasswordRotationUserRetention uint32 `name:"password_rotation_user_retention" default:"180"` PasswordRotationUserRetention uint32 `name:"password_rotation_user_retention" default:"180"`
@@ -128,6 +130,7 @@ type LogicalBackup struct {
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:""` LogicalBackupS3SSE string `name:"logical_backup_s3_sse" default:""`
LogicalBackupS3RetentionTime string `name:"logical_backup_s3_retention_time" default:""`
LogicalBackupGoogleApplicationCredentials string `name:"logical_backup_google_application_credentials" default:""` LogicalBackupGoogleApplicationCredentials string `name:"logical_backup_google_application_credentials" default:""`
LogicalBackupJobPrefix string `name:"logical_backup_job_prefix" default:"logical-backup-"` LogicalBackupJobPrefix string `name:"logical_backup_job_prefix" default:"logical-backup-"`
} }
@@ -189,7 +192,9 @@ type Config struct {
EnablePostgresTeamCRD bool `name:"enable_postgres_team_crd" default:"false"` EnablePostgresTeamCRD bool `name:"enable_postgres_team_crd" default:"false"`
EnablePostgresTeamCRDSuperusers bool `name:"enable_postgres_team_crd_superusers" default:"false"` EnablePostgresTeamCRDSuperusers bool `name:"enable_postgres_team_crd_superusers" default:"false"`
EnableMasterLoadBalancer bool `name:"enable_master_load_balancer" default:"true"` EnableMasterLoadBalancer bool `name:"enable_master_load_balancer" default:"true"`
EnableMasterPoolerLoadBalancer bool `name:"enable_master_pooler_load_balancer" default:"false"`
EnableReplicaLoadBalancer bool `name:"enable_replica_load_balancer" default:"false"` EnableReplicaLoadBalancer bool `name:"enable_replica_load_balancer" default:"false"`
EnableReplicaPoolerLoadBalancer bool `name:"enable_replica_pooler_load_balancer" default:"false"`
CustomServiceAnnotations map[string]string `name:"custom_service_annotations"` CustomServiceAnnotations map[string]string `name:"custom_service_annotations"`
CustomPodAnnotations map[string]string `name:"custom_pod_annotations"` CustomPodAnnotations map[string]string `name:"custom_pod_annotations"`
EnablePodAntiAffinity bool `name:"enable_pod_antiaffinity" default:"false"` EnablePodAntiAffinity bool `name:"enable_pod_antiaffinity" default:"false"`
@@ -210,7 +215,7 @@ type Config struct {
TeamAPIRoleConfiguration map[string]string `name:"team_api_role_configuration" default:"log_statement:all"` TeamAPIRoleConfiguration map[string]string `name:"team_api_role_configuration" default:"log_statement:all"`
PodTerminateGracePeriod time.Duration `name:"pod_terminate_grace_period" default:"5m"` PodTerminateGracePeriod time.Duration `name:"pod_terminate_grace_period" default:"5m"`
PodManagementPolicy string `name:"pod_management_policy" default:"ordered_ready"` PodManagementPolicy string `name:"pod_management_policy" default:"ordered_ready"`
ProtectedRoles []string `name:"protected_role_names" default:"admin"` ProtectedRoles []string `name:"protected_role_names" default:"admin,cron_admin"`
PostgresSuperuserTeams []string `name:"postgres_superuser_teams" default:""` PostgresSuperuserTeams []string `name:"postgres_superuser_teams" default:""`
SetMemoryRequestToLimit bool `name:"set_memory_request_to_limit" default:"false"` SetMemoryRequestToLimit bool `name:"set_memory_request_to_limit" default:"false"`
EnableLazySpiloUpgrade bool `name:"enable_lazy_spilo_upgrade" default:"false"` EnableLazySpiloUpgrade bool `name:"enable_lazy_spilo_upgrade" default:"false"`
@@ -221,6 +226,8 @@ type Config struct {
MajorVersionUpgradeTeamAllowList []string `name:"major_version_upgrade_team_allow_list" default:""` MajorVersionUpgradeTeamAllowList []string `name:"major_version_upgrade_team_allow_list" default:""`
MinimalMajorVersion string `name:"minimal_major_version" default:"9.6"` MinimalMajorVersion string `name:"minimal_major_version" default:"9.6"`
TargetMajorVersion string `name:"target_major_version" default:"14"` TargetMajorVersion string `name:"target_major_version" default:"14"`
PatroniAPICheckInterval time.Duration `name:"patroni_api_check_interval" default:"1s"`
PatroniAPICheckTimeout time.Duration `name:"patroni_api_check_timeout" default:"5s"`
} }
// MustMarshal marshals the config or panics // MustMarshal marshals the config or panics
@@ -287,7 +294,7 @@ func validate(cfg *Config) (err error) {
} }
if cfg.ConnectionPooler.User == cfg.SuperUsername { if cfg.ConnectionPooler.User == cfg.SuperUsername {
msg := "Connection pool user is not allowed to be the same as super user, username: %s" msg := "connection pool user is not allowed to be the same as super user, username: %s"
err = fmt.Errorf(msg, cfg.ConnectionPooler.User) err = fmt.Errorf(msg, cfg.ConnectionPooler.User)
} }
+8 -51
View File
@@ -37,6 +37,14 @@ func Int32ToPointer(value int32) *int32 {
return &value return &value
} }
func UInt32ToPointer(value uint32) *uint32 {
return &value
}
func StringToPointer(str string) *string {
return &str
}
// KubernetesClient describes getters for Kubernetes objects // KubernetesClient describes getters for Kubernetes objects
type KubernetesClient struct { type KubernetesClient struct {
corev1.SecretsGetter corev1.SecretsGetter
@@ -205,57 +213,6 @@ func (client *KubernetesClient) SetPostgresCRDStatus(clusterName spec.Namespaced
return pg, nil return pg, nil
} }
// SameService compares the Services
func SameService(cur, new *v1.Service) (match bool, reason string) {
//TODO: improve comparison
if cur.Spec.Type != new.Spec.Type {
return false, fmt.Sprintf("new service's type %q does not match the current one %q",
new.Spec.Type, cur.Spec.Type)
}
oldSourceRanges := cur.Spec.LoadBalancerSourceRanges
newSourceRanges := new.Spec.LoadBalancerSourceRanges
/* work around Kubernetes 1.6 serializing [] as nil. See https://github.com/kubernetes/kubernetes/issues/43203 */
if (len(oldSourceRanges) != 0) || (len(newSourceRanges) != 0) {
if !reflect.DeepEqual(oldSourceRanges, newSourceRanges) {
return false, "new service's LoadBalancerSourceRange does not match the current one"
}
}
match = true
reasonPrefix := "new service's annotations does not match the current one:"
for ann := range cur.Annotations {
if _, ok := new.Annotations[ann]; !ok {
match = false
if len(reason) == 0 {
reason = reasonPrefix
}
reason += fmt.Sprintf(" Removed '%s'.", ann)
}
}
for ann := range new.Annotations {
v, ok := cur.Annotations[ann]
if !ok {
if len(reason) == 0 {
reason = reasonPrefix
}
reason += fmt.Sprintf(" Added '%s' with value '%s'.", ann, new.Annotations[ann])
match = false
} else if v != new.Annotations[ann] {
if len(reason) == 0 {
reason = reasonPrefix
}
reason += fmt.Sprintf(" '%s' changed from '%s' to '%s'.", ann, v, new.Annotations[ann])
match = false
}
}
return match, reason
}
// SamePDB compares the PodDisruptionBudgets // SamePDB compares the PodDisruptionBudgets
func SamePDB(cur, new *policybeta1.PodDisruptionBudget) (match bool, reason string) { func SamePDB(cur, new *policybeta1.PodDisruptionBudget) (match bool, reason string) {
//TODO: improve comparison //TODO: improve comparison
-311
View File
@@ -1,311 +0,0 @@
package k8sutil
import (
"strings"
"testing"
"github.com/zalando/postgres-operator/pkg/util/constants"
v1 "k8s.io/api/core/v1"
)
func newsService(ann map[string]string, svcT v1.ServiceType, lbSr []string) *v1.Service {
svc := &v1.Service{
Spec: v1.ServiceSpec{
Type: svcT,
LoadBalancerSourceRanges: lbSr,
},
}
svc.Annotations = ann
return svc
}
func TestSameService(t *testing.T) {
tests := []struct {
about string
current *v1.Service
new *v1.Service
reason string
match bool
}{
{
about: "two equal services",
current: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeClusterIP,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeClusterIP,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: true,
},
{
about: "services differ on service type",
current: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeClusterIP,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
reason: `new service's type "LoadBalancer" does not match the current one "ClusterIP"`,
},
{
about: "services differ on lb source ranges",
current: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"185.249.56.0/22"}),
match: false,
reason: `new service's LoadBalancerSourceRange does not match the current one`,
},
{
about: "new service doesn't have lb source ranges",
current: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{}),
match: false,
reason: `new service's LoadBalancerSourceRange does not match the current one`,
},
{
about: "services differ on DNS annotation",
current: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "new_clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
reason: `new service's annotations does not match the current one: 'external-dns.alpha.kubernetes.io/hostname' changed from 'clstr.acid.zalan.do' to 'new_clstr.acid.zalan.do'.`,
},
{
about: "services differ on AWS ELB annotation",
current: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: "1800",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
reason: `new service's annotations does not match the current one: 'service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout' changed from '3600' to '1800'.`,
},
{
about: "service changes existing annotation",
current: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"foo": "bar",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"foo": "baz",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
reason: `new service's annotations does not match the current one: 'foo' changed from 'bar' to 'baz'.`,
},
{
about: "service changes multiple existing annotations",
current: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"foo": "bar",
"bar": "foo",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"foo": "baz",
"bar": "fooz",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
// Test just the prefix to avoid flakiness and map sorting
reason: `new service's annotations does not match the current one:`,
},
{
about: "service adds a new custom annotation",
current: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"foo": "bar",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
reason: `new service's annotations does not match the current one: Added 'foo' with value 'bar'.`,
},
{
about: "service removes a custom annotation",
current: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"foo": "bar",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
reason: `new service's annotations does not match the current one: Removed 'foo'.`,
},
{
about: "service removes a custom annotation and adds a new one",
current: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"foo": "bar",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"bar": "foo",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
reason: `new service's annotations does not match the current one: Removed 'foo'. Added 'bar' with value 'foo'.`,
},
{
about: "service removes a custom annotation, adds a new one and change another",
current: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"foo": "bar",
"zalan": "do",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
"bar": "foo",
"zalan": "do.com",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
// Test just the prefix to avoid flakiness and map sorting
reason: `new service's annotations does not match the current one: Removed 'foo'.`,
},
{
about: "service add annotations",
current: newsService(
map[string]string{},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
new: newsService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue,
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"}),
match: false,
// Test just the prefix to avoid flakiness and map sorting
reason: `new service's annotations does not match the current one: Added `,
},
}
for _, tt := range tests {
t.Run(tt.about, func(t *testing.T) {
match, reason := SameService(tt.current, tt.new)
if match && !tt.match {
t.Errorf("expected services to do not match: '%q' and '%q'", tt.current, tt.new)
return
}
if !match && tt.match {
t.Errorf("expected services to be the same: '%q' and '%q'", tt.current, tt.new)
return
}
if !match && !tt.match {
if !strings.HasPrefix(reason, tt.reason) {
t.Errorf("expected reason prefix '%s', found '%s'", tt.reason, reason)
return
}
}
})
}
}
+14 -8
View File
@@ -53,25 +53,31 @@ func (strategy DefaultUserSyncStrategy) ProduceSyncRequests(dbUsers spec.PgUserM
} }
} else { } else {
r := spec.PgSyncUserRequest{} r := spec.PgSyncUserRequest{}
r.User = dbUser
newMD5Password := util.NewEncryptor(strategy.PasswordEncryption).PGUserPassword(newUser) newMD5Password := util.NewEncryptor(strategy.PasswordEncryption).PGUserPassword(newUser)
if dbUser.Password != newMD5Password { // do not compare for roles coming from docker image
r.User.Password = newMD5Password if newUser.Origin != spec.RoleOriginSpilo {
r.Kind = spec.PGsyncUserAlter if dbUser.Password != newMD5Password {
r.User.Password = newMD5Password
r.Kind = spec.PGsyncUserAlter
}
if addNewFlags, equal := util.SubstractStringSlices(newUser.Flags, dbUser.Flags); !equal {
r.User.Flags = addNewFlags
r.Kind = spec.PGsyncUserAlter
}
} }
if addNewRoles, equal := util.SubstractStringSlices(newUser.MemberOf, dbUser.MemberOf); !equal { if addNewRoles, equal := util.SubstractStringSlices(newUser.MemberOf, dbUser.MemberOf); !equal {
r.User.MemberOf = addNewRoles r.User.MemberOf = addNewRoles
r.Kind = spec.PGsyncUserAlter r.Kind = spec.PGsyncUserAlter
} }
if addNewFlags, equal := util.SubstractStringSlices(newUser.Flags, dbUser.Flags); !equal {
r.User.Flags = addNewFlags
r.Kind = spec.PGsyncUserAlter
}
if r.Kind == spec.PGsyncUserAlter { if r.Kind == spec.PGsyncUserAlter {
r.User.Name = newUser.Name r.User.Name = newUser.Name
reqs = append(reqs, r) reqs = append(reqs, r)
} }
if len(newUser.Parameters) > 0 && !reflect.DeepEqual(dbUser.Parameters, newUser.Parameters) { if newUser.Origin != spec.RoleOriginSpilo &&
len(newUser.Parameters) > 0 &&
!reflect.DeepEqual(dbUser.Parameters, newUser.Parameters) {
reqs = append(reqs, spec.PgSyncUserRequest{Kind: spec.PGSyncAlterSet, User: newUser}) reqs = append(reqs, spec.PgSyncUserRequest{Kind: spec.PGSyncAlterSet, User: newUser})
} }
} }
+11 -2
View File
@@ -126,6 +126,7 @@ edit
if (i.metadata.selfLink) { delete i.metadata.selfLink } if (i.metadata.selfLink) { delete i.metadata.selfLink }
if (i.metadata.uid) { delete i.metadata.uid } if (i.metadata.uid) { delete i.metadata.uid }
if (i.metadata.resourceVersion) { delete i.metadata.resourceVersion } if (i.metadata.resourceVersion) { delete i.metadata.resourceVersion }
if (i.metadata.managedFields) { delete i.metadata.managedFields }
this.update() this.update()
this.refs.yamlNice.innerHTML = yamlParser.safeDump(i.postgresql, {sortKeys: true}) this.refs.yamlNice.innerHTML = yamlParser.safeDump(i.postgresql, {sortKeys: true})
@@ -138,7 +139,15 @@ edit
o.spec.enableMasterLoadBalancer = i.spec.enableMasterLoadBalancer || false o.spec.enableMasterLoadBalancer = i.spec.enableMasterLoadBalancer || false
o.spec.enableReplicaLoadBalancer = i.spec.enableReplicaLoadBalancer || false o.spec.enableReplicaLoadBalancer = i.spec.enableReplicaLoadBalancer || false
o.spec.enableConnectionPooler = i.spec.enableConnectionPooler || false o.spec.enableConnectionPooler = i.spec.enableConnectionPooler || false
o.spec.volume = { size: i.spec.volume.size }
o.spec.volume = {
size: i.spec.volume.size,
throughput: i.spec.volume.throughput || 125,
iops: i.spec.volume.iops || 3000
}
o.spec.postgresql = {}
o.spec.postgresql.version = i.spec.postgresql.version
if ('users' in i.spec && typeof i.spec.users === 'object') { if ('users' in i.spec && typeof i.spec.users === 'object') {
o.spec.users = Object.mapValues(i.spec.users, roleFlags => o.spec.users = Object.mapValues(i.spec.users, roleFlags =>
@@ -166,7 +175,7 @@ edit
].forEach(resourceType => { ].forEach(resourceType => {
if (resourceType in resources) { if (resourceType in resources) {
const resourceClaim = resources[resourceType] const resourceClaim = resources[resourceType]
if (typeof resourceClaim === '') { if (typeof resourceClaim === 'string') {
o.spec.resources[section][resourceType] = resources[resourceType] o.spec.resources[section][resourceType] = resources[resourceType]
} }
} }
+2 -2
View File
@@ -13,6 +13,6 @@ help-general
h3 Basics h3 Basics
p. p.
The PostgreSQL operator will use your definition to create a new The Postgres Operator will use your definition to create a new
PostgreSQL cluster for you. You can either copy the yaml definition PostgreSQL cluster for you. You can either copy the yaml definition
to the repositiory or you can just hit create cluster. to a repositiory or hit create cluster (not available in prod).
+44 -1
View File
@@ -266,6 +266,37 @@ new
) )
.input-group-addon .input-group-addon
.input-units Gi .input-units Gi
tr
td
td Specify Iops and Throughput only if you need more than the default 3000 Iops and 125Mb/s EBS provides.
tr
td Iops
td
.input-group
input.form-control(
ref='iops'
type='number'
value='{ iops }'
onchange='{ iopsChange }'
onkeyup='{ iopsChange }'
)
.input-group-addon
.input-units
tr
td Througput
td
.input-group
input.form-control(
ref='throughput'
type='number'
value='{ throughput }'
onchange='{ throughputChange }'
onkeyup='{ throughputChange }'
)
.input-group-addon
.input-units MB/s
tr(if='{ config.users_visible }') tr(if='{ config.users_visible }')
td td
@@ -509,7 +540,9 @@ new
enableConnectionPooler: true enableConnectionPooler: true
{{/if}} {{/if}}
volume: volume:
size: "{{ volumeSize }}Gi" size: "{{ volumeSize }}Gi"{{#if iops}}
iops: {{ iops }}{{/if}}{{#if throughput}}
throughput: {{ throughput }}{{/if}}
{{#if users}} {{#if users}}
users:{{#each users}} users:{{#each users}}
{{ state }}: []{{/each}}{{/if}} {{ state }}: []{{/each}}{{/if}}
@@ -560,6 +593,8 @@ new
enableReplicaLoadBalancer: this.enableReplicaLoadBalancer, enableReplicaLoadBalancer: this.enableReplicaLoadBalancer,
enableConnectionPooler: this.enableConnectionPooler, enableConnectionPooler: this.enableConnectionPooler,
volumeSize: this.volumeSize, volumeSize: this.volumeSize,
iops: this.iops,
throughput: this.throughput,
users: this.users.valids, users: this.users.valids,
databases: this.databases.valids, databases: this.databases.valids,
ranges: this.ranges, ranges: this.ranges,
@@ -624,6 +659,14 @@ new
this.volumeSize = +e.target.value this.volumeSize = +e.target.value
} }
this.iopsChange = e => {
this.iops = +e.target.value
}
this.throughputChange = e => {
this.throughput = +e.target.value
}
this.updateDNSName = () => { this.updateDNSName = () => {
this.dnsName = this.config.dns_format_string.format( this.dnsName = this.config.dns_format_string.format(
this.name, this.name,
+23 -5
View File
@@ -69,7 +69,7 @@ postgresqls
td { cpu } / { cpu_limit } td { cpu } / { cpu_limit }
td { memory } / { memory_limit } td { memory } / { memory_limit }
td { volume_size } td { volume_size }
td { calcCosts(nodes, cpu, memory, volume_size) }$ td { calcCosts(nodes, cpu, memory, volume_size, iops, throughput) }$
td td
@@ -152,7 +152,7 @@ postgresqls
td { cpu } / { cpu_limit } td { cpu } / { cpu_limit }
td { memory } / { memory_limit } td { memory } / { memory_limit }
td { volume_size } td { volume_size }
td { calcCosts(nodes, cpu, memory, volume_size) }$ td { calcCosts(nodes, cpu, memory, volume_size, iops, throughput) }$
td td
@@ -227,9 +227,22 @@ postgresqls
+ '/' + encodeURI(cluster.name) + '/' + encodeURI(cluster.name)
) )
const calcCosts = this.calcCosts = (nodes, cpu, memory, disk) => { const calcCosts = this.calcCosts = (nodes, cpu, memory, disk, iops, throughput) => {
costs = Math.max(nodes, opts.config.min_pods) * (toCores(cpu) * opts.config.cost_core + toMemory(memory) * opts.config.cost_memory + toDisk(disk) * opts.config.cost_ebs) podcount = Math.max(nodes, opts.config.min_pods)
return costs.toFixed(2) corecost = toCores(cpu) * opts.config.cost_core
memorycost = toMemory(memory) * opts.config.cost_memory
diskcost = toDisk(disk) * opts.config.cost_ebs
iopscost = 0
if (iops !== undefined && iops > 3000) {
iopscost = (iops - 3000) * opts.config.cost_iops
}
throughputcost = 0
if (throughput !== undefined && throughput > 125) {
throughputcost = (throughput - 125) * opts.config.cost_throughput
}
costs = podcount * (corecost + memorycost + diskcost + iopscost + throughputcost)
return costs.toFixed(2)
} }
const toDisk = this.toDisk = value => { const toDisk = this.toDisk = value => {
@@ -253,6 +266,11 @@ postgresqls
value = Number(value) value = Number(value)
return value return value
} }
else if(value.endsWith("Ti")) {
value = value.substring(0, value.length-2)
value = Number(value) * 1000
return value
}
return value return value
} }
+3 -1
View File
@@ -62,7 +62,9 @@ spec:
"replica_load_balancer_visible": true, "replica_load_balancer_visible": true,
"resources_visible": true, "resources_visible": true,
"users_visible": true, "users_visible": true,
"cost_ebs": 0.119, "cost_ebs": 0.0952,
"cost_iops": 0.006,
"cost_throughput": 0.0476,
"cost_core": 0.0575, "cost_core": 0.0575,
"cost_memory": 0.014375, "cost_memory": 0.014375,
"postgresql_versions": [ "postgresql_versions": [
+1
View File
@@ -6,6 +6,7 @@ metadata:
labels: labels:
application: "postgres-operator-ui" application: "postgres-operator-ui"
spec: spec:
# ingressClassName: "ingress-nginx"
rules: rules:
- host: "ui.example.org" - host: "ui.example.org"
http: http:
+51 -2
View File
@@ -89,8 +89,10 @@ TARGET_NAMESPACE = getenv('TARGET_NAMESPACE')
GOOGLE_ANALYTICS = getenv('GOOGLE_ANALYTICS', False) GOOGLE_ANALYTICS = getenv('GOOGLE_ANALYTICS', False)
MIN_PODS= getenv('MIN_PODS', 2) MIN_PODS= getenv('MIN_PODS', 2)
# storage pricing, i.e. https://aws.amazon.com/ebs/pricing/ # storage pricing, i.e. https://aws.amazon.com/ebs/pricing/ (e.g. Europe - Franfurt)
COST_EBS = float(getenv('COST_EBS', 0.119)) # GB per month COST_EBS = float(getenv('COST_EBS', 0.0952)) # GB per month
COST_IOPS = float(getenv('COST_IOPS', 0.006)) # IOPS per month above 3000 baseline
COST_THROUGHPUT = float(getenv('COST_THROUGHPUT', 0.0476)) # MB/s per month above 125 MB/s baseline
# compute costs, i.e. https://www.ec2instances.info/?region=eu-central-1&selected=m5.2xlarge # compute costs, i.e. https://www.ec2instances.info/?region=eu-central-1&selected=m5.2xlarge
COST_CORE = 30.5 * 24 * float(getenv('COST_CORE', 0.0575)) # Core per hour m5.2xlarge / 8. COST_CORE = 30.5 * 24 * float(getenv('COST_CORE', 0.0575)) # Core per hour m5.2xlarge / 8.
@@ -308,6 +310,8 @@ DEFAULT_UI_CONFIG = {
'pgui_link': '', 'pgui_link': '',
'static_network_whitelist': {}, 'static_network_whitelist': {},
'cost_ebs': COST_EBS, 'cost_ebs': COST_EBS,
'cost_iops': COST_IOPS,
'cost_throughput': COST_THROUGHPUT,
'cost_core': COST_CORE, 'cost_core': COST_CORE,
'cost_memory': COST_MEMORY, 'cost_memory': COST_MEMORY,
'min_pods': MIN_PODS 'min_pods': MIN_PODS
@@ -487,6 +491,8 @@ def get_postgresqls():
'cpu': spec.get('resources', {}).get('requests', {}).get('cpu', 0), 'cpu': spec.get('resources', {}).get('requests', {}).get('cpu', 0),
'cpu_limit': spec.get('resources', {}).get('limits', {}).get('cpu', 0), 'cpu_limit': spec.get('resources', {}).get('limits', {}).get('cpu', 0),
'volume_size': spec.get('volume', {}).get('size', 0), 'volume_size': spec.get('volume', {}).get('size', 0),
'iops': spec.get('volume', {}).get('iops', 3000),
'throughput': spec.get('volume', {}).get('throughput', 125),
'team': ( 'team': (
spec.get('teamId') or spec.get('teamId') or
metadata.get('labels', {}).get('team', '') metadata.get('labels', {}).get('team', '')
@@ -614,6 +620,28 @@ def update_postgresql(namespace: str, cluster: str):
spec['volume'] = {'size': size} spec['volume'] = {'size': size}
if (
'volume' in postgresql['spec']
and 'iops' in postgresql['spec']['volume']
and postgresql['spec']['volume']['iops'] != None
):
iops = int(postgresql['spec']['volume']['iops'])
if not 'volume' in spec:
spec['volume'] = {}
spec['volume']['iops'] = iops
if (
'volume' in postgresql['spec']
and 'throughput' in postgresql['spec']['volume']
and postgresql['spec']['volume']['throughput'] != None
):
throughput = int(postgresql['spec']['volume']['throughput'])
if not 'volume' in spec:
spec['volume'] = {}
spec['volume']['throughput'] = throughput
if 'enableConnectionPooler' in postgresql['spec']: if 'enableConnectionPooler' in postgresql['spec']:
cp = postgresql['spec']['enableConnectionPooler'] cp = postgresql['spec']['enableConnectionPooler']
if not cp: if not cp:
@@ -758,6 +786,27 @@ def update_postgresql(namespace: str, cluster: str):
owner_username=owner_username, owner_username=owner_username,
) )
resource_types = ["cpu","memory"]
resource_constraints = ["requests","limits"]
if "resources" in postgresql["spec"]:
spec["resources"] = {}
res = postgresql["spec"]["resources"]
for rt in resource_types:
for rc in resource_constraints:
if rc in res:
if rt in res[rc]:
if not rc in spec["resources"]:
spec["resources"][rc] = {}
spec["resources"][rc][rt] = res[rc][rt]
if "postgresql" in postgresql["spec"]:
if "version" in postgresql["spec"]["postgresql"]:
if "postgresql" not in spec:
spec["postgresql"]={}
spec["postgresql"]["version"] = postgresql["spec"]["postgresql"]["version"]
o['spec'].update(spec) o['spec'].update(spec)
apply_postgresql(get_cluster(), namespace, cluster, o) apply_postgresql(get_cluster(), namespace, cluster, o)
+3 -1
View File
@@ -19,7 +19,9 @@ default_operator_ui_config='{
"nat_gateways_visible": false, "nat_gateways_visible": false,
"resources_visible": true, "resources_visible": true,
"users_visible": true, "users_visible": true,
"cost_ebs": 0.119, "cost_ebs": 0.0952,
"cost_iops": 0.006,
"cost_throughput": 0.0476,
"cost_core": 0.0575, "cost_core": 0.0575,
"cost_memory": 0.014375, "cost_memory": 0.014375,
"postgresql_versions": [ "postgresql_versions": [