Merge branch 'master' into feat/configurable-pooler-securitycontext

This commit is contained in:
Mitch Murphy 2026-07-17 09:47:11 -04:00 committed by GitHub
commit 3e93fae514
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 773 additions and 5177 deletions

View File

@ -29,16 +29,19 @@ PKG := `go list ./... | grep -v /vendor/`
ifeq ($(DEBUG),1)
DOCKERFILE = DebugDockerfile
DEBUG_POSTFIX := -debug-$(shell date hhmmss)
DEBUG_POSTFIX := -debug-$(shell date +"%H%M%S")
BUILD_FLAGS += -gcflags "-N -l"
else
DOCKERFILE = Dockerfile
endif
ifeq ($(FRESH),1)
DEBUG_FRESH=$(shell date +"%H-%M-%S")
endif
SED := $(shell command -v gsed 2>/dev/null || command -v sed)
ifdef CDP_PULL_REQUEST_NUMBER
CDP_TAG := -${CDP_BUILD_VERSION}
endif
@ -69,14 +72,17 @@ $(GENERATED_CRDS): $(GENERATED)
go tool controller-gen crd:crdVersions=v1,allowDangerousTypes=true paths=./pkg/apis/acid.zalan.do/... output:crd:dir=manifests
@mv manifests/acid.zalan.do_postgresqls.yaml manifests/postgresql.crd.yaml
@# hack to use lowercase kind and listKind
@sed -i -e 's/kind: Postgresql/kind: postgresql/' manifests/postgresql.crd.yaml
@sed -i -e 's/listKind: PostgresqlList/listKind: postgresqlList/' manifests/postgresql.crd.yaml
@$(SED) -i -e 's/kind: Postgresql/kind: postgresql/' manifests/postgresql.crd.yaml
@$(SED) -i -e 's/listKind: PostgresqlList/listKind: postgresqlList/' manifests/postgresql.crd.yaml
@hack/adjust_postgresql_crd.sh
@mv manifests/acid.zalan.do_operatorconfigurations.yaml manifests/operatorconfiguration.crd.yaml
@hack/adjust_operatorconfiguration_crd.sh
@mv manifests/acid.zalan.do_postgresteams.yaml manifests/postgresteam.crd.yaml
@cp manifests/postgresql.crd.yaml pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml
@cp manifests/postgresql.crd.yaml charts/postgres-operator/crds/postgresqls.yaml
@cp manifests/operatorconfiguration.crd.yaml pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml
@cp manifests/operatorconfiguration.crd.yaml charts/postgres-operator/crds/operatorconfigurations.yaml
@cp manifests/postgresteam.crd.yaml charts/postgres-operator/crds/postgresteams.yaml
local: ${SOURCES} $(GENERATED_CRDS)
CGO_ENABLED=${CGO_ENABLED} go build -o build/${BINARY} $(LOCAL_BUILD_FLAGS) -ldflags "$(LDFLAGS)" $(SOURCES)

View File

@ -1,9 +1,12 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: operatorconfigurations.acid.zalan.do
annotations:
controller-gen.kubebuilder.io/version: v0.17.3
labels:
app.kubernetes.io/name: postgres-operator
name: operatorconfigurations.acid.zalan.do
spec:
group: acid.zalan.do
names:
@ -70,6 +73,8 @@ spec:
type: integer
gcp_credentials:
type: string
irsa_role_arn:
type: string
kube_iam_role:
type: string
log_s3_bucket:
@ -450,8 +455,7 @@ spec:
default: 20m
description: timeout for successful migration of master pods from
unschedulable node
format: int64
type: integer
type: string
node_readiness_label:
additionalProperties:
type: string
@ -524,8 +528,7 @@ spec:
default: 5m
description: Postgres pods are terminated forcefully after this
timeout
format: int64
type: integer
type: string
secret_name_template:
default: '{username}.{cluster}.credentials.{tprkind}.{tprgroup}'
description: |-
@ -785,13 +788,11 @@ spec:
repair_period:
default: 5m
description: period between consecutive repair requests
format: int64
type: integer
type: string
resync_period:
default: 30m
description: period between consecutive sync requests
format: int64
type: integer
type: string
scalyr:
description: ScalyrConfiguration defines the configuration for ScalyrAPI
properties:
@ -857,7 +858,9 @@ spec:
type: string
type: array
protected_role_names:
default: '["admin", "cron_admin"]'
default:
- admin
- cron_admin
items:
type: string
type: array
@ -885,48 +888,40 @@ spec:
default: 1s
description: interval between consecutive attempts of operator
calling the Patroni API
format: int64
type: integer
type: string
patroni_api_check_timeout:
default: 5s
description: timeout when waiting for successful response from
Patroni API
format: int64
type: integer
type: string
pod_deletion_wait_timeout:
default: 10m
description: timeout when waiting for the Postgres pods to be
deleted
format: int64
type: integer
type: string
pod_label_wait_timeout:
default: 10m
description: timeout when waiting for pod role and cluster labels
format: int64
type: integer
type: string
ready_wait_interval:
default: 4s
description: interval between consecutive attempts waiting for
postgresql CRD to be created
format: int64
type: integer
type: string
ready_wait_timeout:
default: 30s
description: timeout for the complete postgres CRD creation
format: int64
type: integer
type: string
resource_check_interval:
default: 3s
description: interval to wait between consecutive attempts to
check for some K8s resources
format: int64
type: integer
type: string
resource_check_timeout:
default: 10m
description: timeout when waiting for the presence of a certain
K8s resource
format: int64
type: integer
type: string
type: object
users:
description: PostgresUsersConfiguration defines the system users of

File diff suppressed because it is too large Load Diff

View File

@ -1,70 +1,84 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: postgresteams.acid.zalan.do
annotations:
controller-gen.kubebuilder.io/version: v0.17.3
labels:
app.kubernetes.io/name: postgres-operator
name: postgresteams.acid.zalan.do
spec:
group: acid.zalan.do
names:
categories:
- all
kind: PostgresTeam
listKind: PostgresTeamList
plural: postgresteams
singular: postgresteam
shortNames:
- pgteam
categories:
- all
singular: postgresteam
scope: Namespaced
versions:
- name: v1
schema:
openAPIV3Schema:
description: PostgresTeam defines Custom Resource Definition Object for team
management.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: PostgresTeamSpec defines the specification for the PostgresTeam
TPR.
properties:
additionalMembers:
additionalProperties:
description: List of users who will also be added to the Postgres
cluster.
items:
type: string
type: array
description: Map for teamId and associated additional users
type: object
additionalSuperuserTeams:
additionalProperties:
description: List of teams to become Postgres superusers
items:
type: string
type: array
description: Map for teamId and associated additional superuser teams
type: object
additionalTeams:
additionalProperties:
description: List of teams whose members will also be added to the
Postgres cluster.
items:
type: string
type: array
description: Map for teamId and associated additional teams
type: object
type: object
required:
- metadata
- spec
type: object
served: true
storage: true
subresources:
status: {}
schema:
openAPIV3Schema:
type: object
required:
- kind
- apiVersion
- spec
properties:
kind:
type: string
enum:
- PostgresTeam
apiVersion:
type: string
enum:
- acid.zalan.do/v1
spec:
type: object
properties:
additionalSuperuserTeams:
type: object
description: "Map for teamId and associated additional superuser teams"
additionalProperties:
type: array
nullable: true
description: "List of teams to become Postgres superusers"
items:
type: string
additionalTeams:
type: object
description: "Map for teamId and associated additional teams"
additionalProperties:
type: array
nullable: true
description: "List of teams whose members will also be added to the Postgres cluster"
items:
type: string
additionalMembers:
type: object
description: "Map for teamId and associated additional users"
additionalProperties:
type: array
nullable: true
description: "List of users who will also be added to the Postgres cluster"
items:
type: string

View File

@ -234,6 +234,7 @@ rules:
verbs:
- get
- create
- update
# to create role bindings to the postgres-pod service account
- apiGroups:
- rbac.authorization.k8s.io

View File

@ -50,7 +50,7 @@ configGeneral:
# ignore_resources_limits_annotation_key: ""
# Select if setup uses endpoints (default), or configmaps to manage leader (DCS=k8s)
# kubernetes_use_configmaps: false
kubernetes_use_configmaps: true
# maintenance windows applied to all Postgres clusters unless overridden in the manifest
# maintenance_windows:
@ -360,6 +360,8 @@ configAwsOrGcp:
# AWS IAM role to supply in the iam.amazonaws.com/role annotation of Postgres pods
# kube_iam_role: ""
# Full ARN for IRSA (IAM Roles for Service Accounts) on EKS
# irsa_role_arn: ""
# S3 bucket to use for shipping postgres daily logs
# log_s3_bucket: ""
@ -536,7 +538,6 @@ podPriorityClassName:
resources:
limits:
cpu: 500m
memory: 500Mi
requests:
cpu: 100m

View File

@ -1094,6 +1094,32 @@ configuration:
wal_s3_bucket: your-backup-path
```
Alternatively, if your cluster uses EKS with OIDC, you can use
[IRSA](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html)
(IAM Roles for Service Accounts) instead of kube2iam. Set `irsa_role_arn` to
the full ARN of the IAM role:
**OperatorConfiguration**
```yaml
apiVersion: "acid.zalan.do/v1"
kind: OperatorConfiguration
metadata:
name: postgresql-operator-configuration
configuration:
aws_or_gcp:
aws_region: eu-central-1
irsa_role_arn: arn:aws:iam::123456789012:role/postgres-pod-role
wal_s3_bucket: your-backup-path
```
When `irsa_role_arn` is set the operator annotates the pod service account with
`eks.amazonaws.com/role-arn` on every reconcile. The EKS OIDC webhook then
injects an AWS web identity token into each pod, which takes precedence over
the EC2 metadata credentials used by kube2iam. Both `kube_iam_role` and
`irsa_role_arn` can coexist during a migration — existing pods retain the
kube2iam annotation until they are rotated, at which point only IRSA is used.
The referenced IAM role should contain the following privileges to make sure
Postgres can send compressed WAL files to the given S3 bucket:
@ -1204,6 +1230,7 @@ aws_or_gcp:
# additional_secret_mount_path: ""
# aws_region: eu-central-1
# kube_iam_role: ""
# irsa_role_arn: ""
# log_s3_bucket: ""
# wal_s3_bucket: ""
wal_gs_bucket: "postgres-backups-bucket-28302F2" # name of bucket on where to save the WAL-E logs
@ -1253,6 +1280,7 @@ aws_or_gcp:
additional_secret_mount_path: "/var/secrets/google" # or where ever you want to mount the file
# aws_region: eu-central-1
# kube_iam_role: ""
# irsa_role_arn: ""
# log_s3_bucket: ""
# wal_s3_bucket: ""
wal_gs_bucket: "postgres-backups-bucket-28302F2" # name of bucket on where to save the WAL-E logs

View File

@ -27,20 +27,11 @@ git clone https://github.com/zalando/postgres-operator.git
## Building the operator
We use [Go Modules](https://github.com/golang/go/wiki/Modules) for handling
dependencies. When using Go below v1.13 you need to explicitly enable Go modules
by setting the `GO111MODULE` environment variable to `on`. The make targets do
this for you, so simply run
We use [Go Modules](https://github.com/golang/go/wiki/Modules) for handling dependencies.
Run `go mod vendor && go mod tidy` to install them.
```bash
make
```
Build the operator with the `make docker` command. You may define the TAG
variable to assign an explicit tag to your Docker image and the IMAGE to set
the image name. By default, the tag is computed with
`git describe --tags --always --dirty` and the image is
`ghcr.io/zalando/postgres-operator`
Build the operator with the `make docker` command. You may define the TAG variable to assign an explicit tag to your Docker image and the IMAGE to set the image name. By default, the tag is computed with
`git describe --tags --always --dirty` and the image is `ghcr.io/zalando/postgres-operator`. On macos search and replace `sed -i` commands with `sed -i ''` for the make commands to work.
```bash
export TAG=$(git describe --tags --always --dirty)
@ -296,8 +287,7 @@ Please run flake8 [before submitting a PR](http://flake8.pycqa.org/en/latest/use
In the case you want to add functionality to the operator that shall be
controlled via the operator configuration there are a few places that need to
be updated. As explained [here](reference/operator_parameters.md), it's possible
to configure the operator either with a ConfigMap or CRD, but currently we aim
to synchronize parameters everywhere.
to configure the operator either with a ConfigMap or CRD.
When choosing a parameter name for a new option in a Postgres cluster manifest,
keep in mind the naming conventions there. We use `camelCase` for manifest
@ -320,32 +310,28 @@ manifest files:
Postgres manifest parameters are defined in the [api package](https://github.com/zalando/postgres-operator/blob/master/pkg/apis/acid.zalan.do/v1/postgresql_type.go).
The operator behavior has to be implemented at least in [k8sres.go](https://github.com/zalando/postgres-operator/blob/master/pkg/cluster/k8sres.go).
Validation of CRD parameters is controlled in [crds.go](https://github.com/zalando/postgres-operator/blob/master/pkg/apis/acid.zalan.do/v1/crds.go).
Please, reflect your changes in tests, for example in:
* [config_test.go](https://github.com/zalando/postgres-operator/blob/master/pkg/util/config/config_test.go)
* [k8sres_test.go](https://github.com/zalando/postgres-operator/blob/master/pkg/cluster/k8sres_test.go)
* [util_test.go](https://github.com/zalando/postgres-operator/blob/master/pkg/apis/acid.zalan.do/v1/util_test.go)
### Updating manifest files
### Generating the CRDs
For the CRD-based configuration, please update the following files:
* the default [OperatorConfiguration](https://github.com/zalando/postgres-operator/blob/master/manifests/postgresql-operator-default-configuration.yaml)
* the CRD's [validation](https://github.com/zalando/postgres-operator/blob/master/manifests/operatorconfiguration.crd.yaml)
* the CRD's validation in the [Helm chart](https://github.com/zalando/postgres-operator/blob/master/charts/postgres-operator/crds/operatorconfigurations.yaml)
Add new options also to the Helm chart's [values file](https://github.com/zalando/postgres-operator/blob/master/charts/postgres-operator/values.yaml) file.
It follows the OperatorConfiguration CRD layout. Nested values will be flattened for the ConfigMap.
Last but no least, update the [ConfigMap](https://github.com/zalando/postgres-operator/blob/master/manifests/configmap.yaml) manifest example as well.
The CRDs can be automatically generated from the go structs. Use the correct kubebuilder annotations for defining the validation, constraints or default values etc.. Run `make` to update the CRDs which are stored in three locations:
- In the Go api package
- The example manifests folder
- The helm chart folder
### Updating documentation
Finally, add a section for each new configuration option and/or cluster manifest
Config changes need to be reflected in the Helm chart's [values file](https://github.com/zalando/postgres-operator/blob/master/charts/postgres-operator/values.yaml), too. It follows the OperatorConfiguration CRD layout. Nested values will be flattened for the ConfigMap.
Add a section for each new configuration option and/or cluster manifest
parameter in the reference documents:
* [config reference](reference/operator_parameters.md)
* [manifest reference](reference/cluster_manifest.md)
It also helps users to explain new features with examples in the
[administrator docs](administrator.md).
It can also help other K8s admins to explain new features with examples in the
[administrator docs](administrator.md) and also update the [OperatorConfiguration CRD](https://github.com/zalando/postgres-operator/blob/master/manifests/postgresql-operator-default-configuration.yaml) and [ConfigMap](https://github.com/zalando/postgres-operator/blob/master/manifests/configmap.yaml) manifest examples.

29
docs/migrate.md Normal file
View File

@ -0,0 +1,29 @@
<h1>Migrate from v1 to v2</h1>
Version 2.0 changes some default settings and removes deprecated fields. Please read the following sections before upgrading the Postgres Operator deployment.
## K8s Endpoints are deprecated
If your current operator v1.x deployment is relying on K8s endpoints (the default setup) for Patroni to manage the HA state you have to start planning to switch to configmaps, because endpoints are deprecated from K8s 1.33 onwards. The default of the corresponding parameter `kubernetes_use_configmaps` is changing to `true` with v2.0 of the operator. This means you have to explicity set it to `false` in your configuration before you start the upgrade.
We explicitly warn you to go straight to configmap-based HA management with database clusters that use replicas, because there's is a danger to run into split-brain scenarios during the rolling update of pods when there exists a leader endpoint and leader config map at the same time. To play it safe, here is what you should do - before or after the Postgres Operator upgrade:
1. Scale-in all your database clusters to only one primary instance. This can be done by changing the global config options `max_instances` and `min_instances` to `1`. If you have allowed users to ignore globally defined instance limits by configuring an `ignore_instance_limits_annotation_key`, remove it for now.
2. Wait for all clusters to be healthy and change the `kubernetes_use_configmaps` setting to `true`. This will trigger the replacement of the primary pod of all clusters and cause downtime for as long as the pods are rescheduled and start up.
3. Check again that all clusters are healthy with configmaps created. There should be three for each cluster called like cluster name with suffixes `-config`, `-failover` and `-leader`. Now, revert the changes from step 1 and scale-out the to number of instances set in the manifests.
4. The orphaned endpoints, which use the same names like the new configmaps, have to be deleted by you or your K8s garbage collection.
## Dropped manifest fields
We removed some deprecated fields from the Postgresql CRD. Please, make sure that you do not specify them in any of your cluster manifests. If you do, switch to the listed alternative:
| Removed field in v2 | Alternative |
| --- | --- |
| init_containers | initContainers |
| pod_priority_class_name | podPriorityClassName |
| replicaLoadBalancer | enableReplicaLoadBalancer|
| useLoadBalancer | enableMasterLoadBalancer |

View File

@ -42,14 +42,14 @@ and change it.
To test the CRD-based configuration locally, use the following
```bash
kubectl create -f manifests/operatorconfiguration.crd.yaml # registers the CRD
kubectl create -f manifests/postgresql-operator-default-configuration.yaml
```
kubectl create -f manifests/operatorconfiguration.crd.yaml # registers the CRD
kubectl create -f manifests/postgresql-operator-default-configuration.yaml
kubectl create -f manifests/operator-service-account-rbac.yaml
kubectl create -f manifests/postgres-operator.yaml # set the env var as mentioned above
kubectl create -f manifests/operator-service-account-rbac.yaml
kubectl create -f manifests/postgres-operator.yaml # set the env var as mentioned above
kubectl get operatorconfigurations postgresql-operator-default-configuration -o yaml
kubectl get operatorconfigurations postgresql-operator-default-configuration -o yaml
```
The CRD-based configuration is more powerful than the one based on ConfigMaps
@ -100,15 +100,13 @@ Those are top-level keys, containing both leaf keys and groups.
Kubernetes-native DCS).
* **kubernetes_use_configmaps**
Select if setup uses endpoints (default), or configmaps to manage leader when
Select if setup uses endpoints or configmaps (default) to manage leader when
DCS is kubernetes (not etcd or similar). In OpenShift it is not possible to
use endpoints option, and configmaps is required. Starting with K8s 1.33,
endpoints are marked as deprecated. It's recommended to switch to config maps
instead. But, to do so make sure you scale the Postgres cluster down to just
one primary pod (e.g. using `max_instances` option). Otherwise, you risk
running into a split-brain scenario.
By default, `kubernetes_use_configmaps: false`, meaning endpoints will be used.
Starting from v1.16.0 the default will be changed to `true`.
running into a split-brain scenario. Default is `true`.
* **docker_image**
Spilo Docker image for Postgres instances. For production, don't rely on the
@ -800,6 +798,15 @@ yet officially supported.
[kube2iam](https://github.com/jtblin/kube2iam) project on AWS. The default is
empty.
* **irsa_role_arn**
Full AWS IAM role ARN to supply in the `eks.amazonaws.com/role-arn` annotation
of the Postgres pod service account, enabling
[IRSA](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html)
(IAM Roles for Service Accounts) on EKS. When set, the operator annotates the
pod service account on every sync so that the EKS OIDC webhook can inject AWS
credentials directly into pods. Must be a full ARN, e.g.
`arn:aws:iam::123456789012:role/my-postgres-role`. The default is empty.
* **aws_region**
AWS region used to store EBS volumes. The default is `eu-central-1`. Note,
this option is not meant for specifying the AWS region for backups and

View File

@ -1430,7 +1430,7 @@ class EndToEndTestCase(unittest.TestCase):
k8s.api.custom_objects_api.patch_namespaced_custom_object(
"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")
"Operator does not get in sync", retries=120)
# wait for switched over
k8s.wait_for_pod_failover(replica_nodes, 'spilo-role=master,' + cluster_label)

View File

@ -13,12 +13,14 @@
file="${1:-"manifests/postgresql.crd.yaml"}"
sed -i '/^[[:space:]]*standby:$/{
SED=$(command -v gsed 2>/dev/null || command -v sed)
$SED -i '/^[[:space:]]*standby:$/{
# Capture the indentation
s/^\([[:space:]]*\)standby:$/\1standby:\n\1 anyOf:\n\1 - required:\n\1 - s3_wal_path\n\1 - required:\n\1 - gs_wal_path\n\1 - required:\n\1 - standby_host\n\1 not:\n\1 required:\n\1 - s3_wal_path\n\1 - gs_wal_path/
}' "$file"
sed -i '/^[[:space:]]*maintenanceWindows:$/{
$SED -i '/^[[:space:]]*maintenanceWindows:$/{
# Capture the indentation
s/^\([[:space:]]*\)maintenanceWindows:$/\1maintenanceWindows:\n\1 items:\n\1 pattern: '\''^\\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\\d):([0-5]?\\d)|(2[0-3]|[01]?\\d):([0-5]?\\d))-((2[0-3]|[01]?\\d):([0-5]?\\d)|(2[0-3]|[01]?\\d):([0-5]?\\d))\\ *$'\''\n\1 type: string/
}' "$file"

View File

@ -79,6 +79,7 @@ data:
# inherited_annotations: owned-by
# inherited_labels: application,environment
# kube_iam_role: ""
# irsa_role_arn: ""
kubernetes_use_configmaps: "false"
# log_s3_bucket: ""
# logical_backup_azure_storage_account_name: ""

View File

@ -4,6 +4,8 @@ kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.3
labels:
app.kubernetes.io/name: postgres-operator
name: operatorconfigurations.acid.zalan.do
spec:
group: acid.zalan.do
@ -71,6 +73,8 @@ spec:
type: integer
gcp_credentials:
type: string
irsa_role_arn:
type: string
kube_iam_role:
type: string
log_s3_bucket:
@ -445,8 +449,7 @@ spec:
default: 20m
description: timeout for successful migration of master pods from
unschedulable node
format: int64
type: integer
type: string
node_readiness_label:
additionalProperties:
type: string
@ -519,8 +522,7 @@ spec:
default: 5m
description: Postgres pods are terminated forcefully after this
timeout
format: int64
type: integer
type: string
secret_name_template:
default: '{username}.{cluster}.credentials.{tprkind}.{tprgroup}'
description: |-
@ -780,13 +782,11 @@ spec:
repair_period:
default: 5m
description: period between consecutive repair requests
format: int64
type: integer
type: string
resync_period:
default: 30m
description: period between consecutive sync requests
format: int64
type: integer
type: string
scalyr:
description: ScalyrConfiguration defines the configuration for ScalyrAPI
properties:
@ -852,7 +852,9 @@ spec:
type: string
type: array
protected_role_names:
default: '["admin", "cron_admin"]'
default:
- admin
- cron_admin
items:
type: string
type: array
@ -880,48 +882,40 @@ spec:
default: 1s
description: interval between consecutive attempts of operator
calling the Patroni API
format: int64
type: integer
type: string
patroni_api_check_timeout:
default: 5s
description: timeout when waiting for successful response from
Patroni API
format: int64
type: integer
type: string
pod_deletion_wait_timeout:
default: 10m
description: timeout when waiting for the Postgres pods to be
deleted
format: int64
type: integer
type: string
pod_label_wait_timeout:
default: 10m
description: timeout when waiting for pod role and cluster labels
format: int64
type: integer
type: string
ready_wait_interval:
default: 4s
description: interval between consecutive attempts waiting for
postgresql CRD to be created
format: int64
type: integer
type: string
ready_wait_timeout:
default: 30s
description: timeout for the complete postgres CRD creation
format: int64
type: integer
type: string
resource_check_interval:
default: 3s
description: interval to wait between consecutive attempts to
check for some K8s resources
format: int64
type: integer
type: string
resource_check_timeout:
default: 10m
description: timeout when waiting for the presence of a certain
K8s resource
format: int64
type: integer
type: string
type: object
users:
description: PostgresUsersConfiguration defines the system users of

View File

@ -16,7 +16,7 @@ configuration:
etcd_host: ""
# ignore_instance_limits_annotation_key: ""
# ignore_resources_limits_annotation_key: ""
# kubernetes_use_configmaps: false
kubernetes_use_configmaps: true
# maintenance_windows:
# - "Sat:22:00-23:59"
# - "Sun:00:00-01:00"
@ -171,6 +171,7 @@ configuration:
# enable_ebs_gp3_migration_max_size: 1000
# gcp_credentials: ""
# kube_iam_role: ""
# irsa_role_arn: ""
# log_s3_bucket: ""
# wal_az_storage_account: ""
# wal_gs_bucket: ""

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,8 @@ kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.3
labels:
app.kubernetes.io/name: postgres-operator
name: postgresteams.acid.zalan.do
spec:
group: acid.zalan.do

View File

@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"strings"
"time"
)
type postgresqlCopy Postgresql
@ -120,29 +119,3 @@ func (p *Postgresql) UnmarshalJSON(data []byte) error {
return nil
}
// UnmarshalJSON convert to Duration from byte slice of json
func (d *Duration) UnmarshalJSON(b []byte) error {
var (
v interface{}
err error
)
if err = json.Unmarshal(b, &v); err != nil {
return err
}
switch val := v.(type) {
case string:
t, err := time.ParseDuration(val)
if err != nil {
return err
}
*d = Duration(t)
return nil
case float64:
t := time.Duration(val)
*d = Duration(t)
return nil
default:
return fmt.Errorf("could not recognize type %T as a valid type to unmarshal to Duration", val)
}
}

View File

@ -5,8 +5,6 @@ package v1
import (
"github.com/zalando/postgres-operator/pkg/util/config"
"time"
"github.com/zalando/postgres-operator/pkg/spec"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -24,6 +22,7 @@ import (
// +kubebuilder:printcolumn:name="Min-Instances",type=integer,JSONPath=`.configuration.min_instances`,description="Minimum number of instances per Postgres cluster"
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,description="Age of the OperatorConfiguration resource"
// +kubebuilder:subresource:status
// +kubebuilder:metadata:labels=app.kubernetes.io/name=postgres-operator
type OperatorConfiguration struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata"`
@ -77,7 +76,7 @@ type KubernetesMetaConfiguration struct {
PodServiceAccountRoleBindingDefinition string `json:"pod_service_account_role_binding_definition,omitempty"`
// +kubebuilder:default="5m"
// Postgres pods are terminated forcefully after this timeout
PodTerminateGracePeriod Duration `json:"pod_terminate_grace_period,omitempty"`
PodTerminateGracePeriod *metav1.Duration `json:"pod_terminate_grace_period,omitempty"`
// +optional
LivenessProbe *v1.Probe `json:"liveness_probe"`
SpiloPrivileged bool `json:"spilo_privileged,omitempty"`
@ -142,9 +141,9 @@ type KubernetesMetaConfiguration struct {
PodPriorityClassName string `json:"pod_priority_class_name,omitempty"`
// +kubebuilder:default="20m"
// timeout for successful migration of master pods from unschedulable node
MasterPodMoveTimeout Duration `json:"master_pod_move_timeout,omitempty"`
EnablePodAntiAffinity bool `json:"enable_pod_antiaffinity,omitempty"`
PodAntiAffinityPreferredDuringScheduling bool `json:"pod_antiaffinity_preferred_during_scheduling,omitempty"`
MasterPodMoveTimeout *metav1.Duration `json:"master_pod_move_timeout,omitempty"`
EnablePodAntiAffinity bool `json:"enable_pod_antiaffinity,omitempty"`
PodAntiAffinityPreferredDuringScheduling bool `json:"pod_antiaffinity_preferred_during_scheduling,omitempty"`
// +kubebuilder:default="kubernetes.io/hostname"
PodAntiAffinityTopologyKey string `json:"pod_antiaffinity_topology_key,omitempty"`
// +kubebuilder:validation:Enum=ordered_ready;parallel
@ -185,28 +184,28 @@ type PostgresPodResourcesDefaults struct {
type OperatorTimeouts struct {
// +kubebuilder:default="3s"
// interval to wait between consecutive attempts to check for some K8s resources
ResourceCheckInterval Duration `json:"resource_check_interval,omitempty"`
ResourceCheckInterval *metav1.Duration `json:"resource_check_interval,omitempty"`
// +kubebuilder:default="10m"
// timeout when waiting for the presence of a certain K8s resource
ResourceCheckTimeout Duration `json:"resource_check_timeout,omitempty"`
ResourceCheckTimeout *metav1.Duration `json:"resource_check_timeout,omitempty"`
// +kubebuilder:default="10m"
// timeout when waiting for pod role and cluster labels
PodLabelWaitTimeout Duration `json:"pod_label_wait_timeout,omitempty"`
PodLabelWaitTimeout *metav1.Duration `json:"pod_label_wait_timeout,omitempty"`
// +kubebuilder:default="10m"
// timeout when waiting for the Postgres pods to be deleted
PodDeletionWaitTimeout Duration `json:"pod_deletion_wait_timeout,omitempty"`
PodDeletionWaitTimeout *metav1.Duration `json:"pod_deletion_wait_timeout,omitempty"`
// +kubebuilder:default="4s"
// interval between consecutive attempts waiting for postgresql CRD to be created
ReadyWaitInterval Duration `json:"ready_wait_interval,omitempty"`
ReadyWaitInterval *metav1.Duration `json:"ready_wait_interval,omitempty"`
// +kubebuilder:default="30s"
// timeout for the complete postgres CRD creation
ReadyWaitTimeout Duration `json:"ready_wait_timeout,omitempty"`
ReadyWaitTimeout *metav1.Duration `json:"ready_wait_timeout,omitempty"`
// +kubebuilder:default="1s"
// interval between consecutive attempts of operator calling the Patroni API
PatroniAPICheckInterval Duration `json:"patroni_api_check_interval,omitempty"`
PatroniAPICheckInterval *metav1.Duration `json:"patroni_api_check_interval,omitempty"`
// +kubebuilder:default="5s"
// timeout when waiting for successful response from Patroni API
PatroniAPICheckTimeout Duration `json:"patroni_api_check_timeout,omitempty"`
PatroniAPICheckTimeout *metav1.Duration `json:"patroni_api_check_timeout,omitempty"`
}
// LoadBalancerConfiguration defines the LB configuration
@ -253,6 +252,7 @@ type AWSGCPConfiguration struct {
WALAZStorageAccount string `json:"wal_az_storage_account,omitempty"`
LogS3Bucket string `json:"log_s3_bucket,omitempty"`
KubeIAMRole string `json:"kube_iam_role,omitempty"`
IRSARoleARN string `json:"irsa_role_arn,omitempty"`
AdditionalSecretMount string `json:"additional_secret_mount,omitempty"`
AdditionalSecretMountPath string `json:"additional_secret_mount_path,omitempty"`
EnableEBSGp3Migration bool `json:"enable_ebs_gp3_migration,omitempty"`
@ -283,7 +283,7 @@ type TeamsAPIConfiguration struct {
PamRoleName string `json:"pam_role_name,omitempty"`
// +kubebuilder:default="https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees"
PamConfiguration string `json:"pam_configuration,omitempty"`
// +kubebuilder:default="[\"admin\", \"cron_admin\"]"
// +kubebuilder:default={"admin", "cron_admin"}
ProtectedRoles []string `json:"protected_role_names,omitempty"`
PostgresSuperuserTeams []string `json:"postgres_superuser_teams,omitempty"`
// +kubebuilder:default=true
@ -419,7 +419,7 @@ type OperatorConfigurationData struct {
// +kubebuilder:default=""
EtcdHost string `json:"etcd_host,omitempty"`
// +kubebuilder:default=true
KubernetesUseConfigMaps bool `json:"kubernetes_use_configmaps,omitempty"`
KubernetesUseConfigMaps *bool `json:"kubernetes_use_configmaps,omitempty"`
// +kubebuilder:default="ghcr.io/zalando/spilo-18:4.1-p1"
DockerImage string `json:"docker_image,omitempty"`
// +kubebuilder:validation:Minimum=1
@ -427,16 +427,16 @@ type OperatorConfigurationData struct {
Workers uint32 `json:"workers,omitempty"`
// +kubebuilder:default="30m"
// period between consecutive sync requests
ResyncPeriod Duration `json:"resync_period,omitempty"`
ResyncPeriod *metav1.Duration `json:"resync_period,omitempty"`
// +kubebuilder:default="5m"
// period between consecutive repair requests
RepairPeriod Duration `json:"repair_period,omitempty"`
RepairPeriod *metav1.Duration `json:"repair_period,omitempty"`
// +kubebuilder:default=true
EnableMaintenanceWindows *bool `json:"enable_maintenance_windows,omitempty"`
// +kubebuilder:validation:Schemaless
// +kubebuilder:validation:Type=array
MaintenanceWindows []MaintenanceWindow `json:"maintenance_windows,omitempty"`
SetMemoryRequestToLimit bool `json:"set_memory_request_to_limit,omitempty"`
// +kubebuilder:validation:items:Pattern=`^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ *$`
MaintenanceWindows []string `json:"maintenance_windows,omitempty"`
SetMemoryRequestToLimit bool `json:"set_memory_request_to_limit,omitempty"`
// +kubebuilder:default=true
ShmVolume *bool `json:"enable_shm_volume,omitempty"`
SidecarImages map[string]string `json:"sidecar_docker_images,omitempty"` // deprecated in favour of SidecarContainers
@ -485,6 +485,3 @@ type OperatorConfigurationData struct {
IgnoreInstanceLimitsAnnotationKey string `json:"ignore_instance_limits_annotation_key,omitempty"`
IgnoreResourcesLimitsAnnotationKey string `json:"ignore_resources_limits_annotation_key,omitempty"`
}
// Duration shortens this frequently used name
type Duration time.Duration

View File

@ -4,6 +4,8 @@ kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.3
labels:
app.kubernetes.io/name: postgres-operator
name: operatorconfigurations.acid.zalan.do
spec:
group: acid.zalan.do
@ -71,6 +73,8 @@ spec:
type: integer
gcp_credentials:
type: string
irsa_role_arn:
type: string
kube_iam_role:
type: string
log_s3_bucket:
@ -445,8 +449,7 @@ spec:
default: 20m
description: timeout for successful migration of master pods from
unschedulable node
format: int64
type: integer
type: string
node_readiness_label:
additionalProperties:
type: string
@ -519,8 +522,7 @@ spec:
default: 5m
description: Postgres pods are terminated forcefully after this
timeout
format: int64
type: integer
type: string
secret_name_template:
default: '{username}.{cluster}.credentials.{tprkind}.{tprgroup}'
description: |-
@ -780,13 +782,11 @@ spec:
repair_period:
default: 5m
description: period between consecutive repair requests
format: int64
type: integer
type: string
resync_period:
default: 30m
description: period between consecutive sync requests
format: int64
type: integer
type: string
scalyr:
description: ScalyrConfiguration defines the configuration for ScalyrAPI
properties:
@ -852,7 +852,9 @@ spec:
type: string
type: array
protected_role_names:
default: '["admin", "cron_admin"]'
default:
- admin
- cron_admin
items:
type: string
type: array
@ -880,48 +882,40 @@ spec:
default: 1s
description: interval between consecutive attempts of operator
calling the Patroni API
format: int64
type: integer
type: string
patroni_api_check_timeout:
default: 5s
description: timeout when waiting for successful response from
Patroni API
format: int64
type: integer
type: string
pod_deletion_wait_timeout:
default: 10m
description: timeout when waiting for the Postgres pods to be
deleted
format: int64
type: integer
type: string
pod_label_wait_timeout:
default: 10m
description: timeout when waiting for pod role and cluster labels
format: int64
type: integer
type: string
ready_wait_interval:
default: 4s
description: interval between consecutive attempts waiting for
postgresql CRD to be created
format: int64
type: integer
type: string
ready_wait_timeout:
default: 30s
description: timeout for the complete postgres CRD creation
format: int64
type: integer
type: string
resource_check_interval:
default: 3s
description: interval to wait between consecutive attempts to
check for some K8s resources
format: int64
type: integer
type: string
resource_check_timeout:
default: 10m
description: timeout when waiting for the presence of a certain
K8s resource
format: int64
type: integer
type: string
type: object
users:
description: PostgresUsersConfiguration defines the system users of

View File

@ -11,6 +11,7 @@ import (
// +k8s:deepcopy-gen=true
// +kubebuilder:resource:shortName=pgteam,categories=all
// +kubebuilder:subresource:status
// +kubebuilder:metadata:labels=app.kubernetes.io/name=postgres-operator
type PostgresTeam struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata"`

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,7 @@ import (
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,description="Age of the PostgreSQL cluster"
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.PostgresClusterStatus`,description="Current sync status of postgresql resource"
// +kubebuilder:subresource:status
// +kubebuilder:metadata:labels=app.kubernetes.io/name=postgres-operator
type Postgresql struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata"`
@ -75,12 +76,6 @@ type PostgresSpec struct {
EnableReplicaPoolerNodePort *bool `json:"enableReplicaPoolerNodePort,omitempty"`
ReplicaPoolerNodePort *int32 `json:"replicaPoolerNodePort,omitempty"`
// deprecated load balancer settings maintained for backward compatibility
// see "Load balancers" operator docs
UseLoadBalancer *bool `json:"useLoadBalancer,omitempty"`
// deprecated
ReplicaLoadBalancer *bool `json:"replicaLoadBalancer,omitempty"`
// load balancers' source ranges are the same for master and replica services
// +nullable
// +kubebuilder:validation:items:Pattern=`^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])|(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9]))$`
@ -129,11 +124,6 @@ type PostgresSpec struct {
AdditionalVolumes []AdditionalVolume `json:"additionalVolumes,omitempty"`
Streams []Stream `json:"streams,omitempty"`
Env []v1.EnvVar `json:"env,omitempty"`
// deprecated
InitContainersOld []v1.Container `json:"init_containers,omitempty"`
// deprecated
PodPriorityClassNameOld string `json:"pod_priority_class_name,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

View File

@ -9,7 +9,6 @@ import (
"testing"
"time"
"github.com/zalando/postgres-operator/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -178,7 +177,8 @@ var unmarshalCluster = []struct {
"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},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":"Invalid"}`),
err: nil},
err: nil,
},
{
about: "example with /status subresource",
in: []byte(`{
@ -199,156 +199,8 @@ var unmarshalCluster = []struct {
"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},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":{"PostgresClusterStatus":"Invalid"}}`),
err: nil},
{
about: "example with detailed input manifest and deprecated pod_priority_class_name -> podPriorityClassName",
in: []byte(`{
"kind": "Postgresql",
"apiVersion": "acid.zalan.do/v1",
"metadata": {
"name": "acid-testcluster1"
},
"spec": {
"teamId": "acid",
"pod_priority_class_name": "spilo-pod-priority",
"volume": {
"size": "5Gi",
"storageClass": "SSD",
"subPath": "subdir"
},
"numberOfInstances": 2,
"users": {
"zalando": [
"superuser",
"createdb"
]
},
"allowedSourceRanges": [
"127.0.0.1/32"
],
"postgresql": {
"version": "18",
"parameters": {
"shared_buffers": "32MB",
"max_connections": "10",
"log_statement": "all"
}
},
"resources": {
"requests": {
"cpu": "10m",
"memory": "50Mi"
},
"limits": {
"cpu": "300m",
"memory": "3000Mi"
}
},
"clone" : {
"cluster": "acid-batman"
},
"enableShmVolume": false,
"patroni": {
"initdb": {
"encoding": "UTF8",
"locale": "en_US.UTF-8",
"data-checksums": "true"
},
"pg_hba": [
"hostssl all all 0.0.0.0/0 md5",
"host all all 0.0.0.0/0 md5"
],
"ttl": 30,
"loop_wait": 10,
"retry_timeout": 10,
"maximum_lag_on_failover": 33554432,
"slots" : {
"permanent_logical_1" : {
"type" : "logical",
"database" : "foo",
"plugin" : "pgoutput"
}
}
},
"maintenanceWindows": [
"Mon:01:00-06:00",
"Sat:00:00-04:00",
"05:00-05:15"
]
}
}`),
out: Postgresql{
TypeMeta: metav1.TypeMeta{
Kind: "Postgresql",
APIVersion: "acid.zalan.do/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "acid-testcluster1",
},
Spec: PostgresSpec{
PostgresqlParam: PostgresqlParam{
PgVersion: "18",
Parameters: map[string]string{
"shared_buffers": "32MB",
"max_connections": "10",
"log_statement": "all",
},
},
PodPriorityClassNameOld: "spilo-pod-priority",
Volume: Volume{
Size: "5Gi",
StorageClass: "SSD",
SubPath: "subdir",
},
ShmVolume: util.False(),
Patroni: Patroni{
InitDB: map[string]string{
"encoding": "UTF8",
"locale": "en_US.UTF-8",
"data-checksums": "true",
},
PgHba: []string{"hostssl all all 0.0.0.0/0 md5", "host all all 0.0.0.0/0 md5"},
TTL: 30,
LoopWait: 10,
RetryTimeout: 10,
MaximumLagOnFailover: 33554432,
Slots: map[string]map[string]string{"permanent_logical_1": {"type": "logical", "database": "foo", "plugin": "pgoutput"}},
},
Resources: &Resources{
ResourceRequests: ResourceDescription{CPU: stringToPointer("10m"), Memory: stringToPointer("50Mi")},
ResourceLimits: ResourceDescription{CPU: stringToPointer("300m"), Memory: stringToPointer("3000Mi")},
},
TeamID: "acid",
AllowedSourceRanges: []string{"127.0.0.1/32"},
NumberOfInstances: 2,
Users: map[string]UserFlags{"zalando": {"superuser", "createdb"}},
MaintenanceWindows: []MaintenanceWindow{{
Everyday: false,
Weekday: time.Monday,
StartTime: mustParseTime("01:00"),
EndTime: mustParseTime("06:00"),
}, {
Everyday: false,
Weekday: time.Saturday,
StartTime: mustParseTime("00:00"),
EndTime: mustParseTime("04:00"),
},
{
Everyday: true,
Weekday: time.Sunday,
StartTime: mustParseTime("05:00"),
EndTime: mustParseTime("05:15"),
},
},
Clone: &CloneDescription{
ClusterName: "acid-batman",
},
},
Error: "",
},
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"18","parameters":{"log_statement":"all","max_connections":"10","shared_buffers":"32MB"}},"pod_priority_class_name":"spilo-pod-priority","volume":{"size":"5Gi","storageClass":"SSD", "subPath": "subdir"},"enableShmVolume":false,"patroni":{"initdb":{"data-checksums":"true","encoding":"UTF8","locale":"en_US.UTF-8"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"],"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"slots":{"permanent_logical_1":{"database":"foo","plugin":"pgoutput","type":"logical"}}},"resources":{"requests":{"cpu":"10m","memory":"50Mi"},"limits":{"cpu":"300m","memory":"3000Mi"}},"teamId":"acid","allowedSourceRanges":["127.0.0.1/32"],"numberOfInstances":2,"users":{"zalando":["superuser","createdb"]},"maintenanceWindows":["Mon:01:00-06:00","Sat:00:00-04:00","05:00-05:15"],"clone":{"cluster":"acid-batman"}},"status":{"PostgresClusterStatus":""}}`),
err: nil},
err: nil,
},
{
about: "example with clone",
in: []byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1","metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": "acid", "clone": {"cluster": "team-batman"}}}`),
@ -369,7 +221,8 @@ var unmarshalCluster = []struct {
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},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{"cluster":"team-batman"}},"status":{"PostgresClusterStatus":""}}`),
err: nil},
err: nil,
},
{
about: "standby example",
in: []byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1","metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": "acid", "standby": {"s3_wal_path": "s3://custom/path/to/bucket/"}}}`),
@ -390,7 +243,8 @@ var unmarshalCluster = []struct {
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},"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",
in: []byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1"`),

View File

@ -173,6 +173,11 @@ func (in *KubernetesMetaConfiguration) DeepCopyInto(out *KubernetesMetaConfigura
*out = new(bool)
**out = **in
}
if in.PodTerminateGracePeriod != nil {
in, out := &in.PodTerminateGracePeriod, &out.PodTerminateGracePeriod
*out = new(metav1.Duration)
**out = **in
}
if in.LivenessProbe != nil {
in, out := &in.LivenessProbe, &out.LivenessProbe
*out = new(corev1.Probe)
@ -290,6 +295,11 @@ func (in *KubernetesMetaConfiguration) DeepCopyInto(out *KubernetesMetaConfigura
}
}
out.PodEnvironmentConfigMap = in.PodEnvironmentConfigMap
if in.MasterPodMoveTimeout != nil {
in, out := &in.MasterPodMoveTimeout, &out.MasterPodMoveTimeout
*out = new(metav1.Duration)
**out = **in
}
if in.PersistentVolumeClaimRetentionPolicy != nil {
in, out := &in.PersistentVolumeClaimRetentionPolicy, &out.PersistentVolumeClaimRetentionPolicy
*out = make(map[string]string, len(*in))
@ -443,6 +453,21 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.KubernetesUseConfigMaps != nil {
in, out := &in.KubernetesUseConfigMaps, &out.KubernetesUseConfigMaps
*out = new(bool)
**out = **in
}
if in.ResyncPeriod != nil {
in, out := &in.ResyncPeriod, &out.ResyncPeriod
*out = new(metav1.Duration)
**out = **in
}
if in.RepairPeriod != nil {
in, out := &in.RepairPeriod, &out.RepairPeriod
*out = new(metav1.Duration)
**out = **in
}
if in.EnableMaintenanceWindows != nil {
in, out := &in.EnableMaintenanceWindows, &out.EnableMaintenanceWindows
*out = new(bool)
@ -450,10 +475,8 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData
}
if in.MaintenanceWindows != nil {
in, out := &in.MaintenanceWindows, &out.MaintenanceWindows
*out = make([]MaintenanceWindow, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.ShmVolume != nil {
in, out := &in.ShmVolume, &out.ShmVolume
@ -478,7 +501,7 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData
in.MajorVersionUpgrade.DeepCopyInto(&out.MajorVersionUpgrade)
in.Kubernetes.DeepCopyInto(&out.Kubernetes)
out.PostgresPodResources = in.PostgresPodResources
out.Timeouts = in.Timeouts
in.Timeouts.DeepCopyInto(&out.Timeouts)
in.LoadBalancer.DeepCopyInto(&out.LoadBalancer)
out.AWSGCP = in.AWSGCP
in.OperatorDebug.DeepCopyInto(&out.OperatorDebug)
@ -594,6 +617,46 @@ func (in *OperatorLogicalBackupConfiguration) DeepCopy() *OperatorLogicalBackupC
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OperatorTimeouts) DeepCopyInto(out *OperatorTimeouts) {
*out = *in
if in.ResourceCheckInterval != nil {
in, out := &in.ResourceCheckInterval, &out.ResourceCheckInterval
*out = new(metav1.Duration)
**out = **in
}
if in.ResourceCheckTimeout != nil {
in, out := &in.ResourceCheckTimeout, &out.ResourceCheckTimeout
*out = new(metav1.Duration)
**out = **in
}
if in.PodLabelWaitTimeout != nil {
in, out := &in.PodLabelWaitTimeout, &out.PodLabelWaitTimeout
*out = new(metav1.Duration)
**out = **in
}
if in.PodDeletionWaitTimeout != nil {
in, out := &in.PodDeletionWaitTimeout, &out.PodDeletionWaitTimeout
*out = new(metav1.Duration)
**out = **in
}
if in.ReadyWaitInterval != nil {
in, out := &in.ReadyWaitInterval, &out.ReadyWaitInterval
*out = new(metav1.Duration)
**out = **in
}
if in.ReadyWaitTimeout != nil {
in, out := &in.ReadyWaitTimeout, &out.ReadyWaitTimeout
*out = new(metav1.Duration)
**out = **in
}
if in.PatroniAPICheckInterval != nil {
in, out := &in.PatroniAPICheckInterval, &out.PatroniAPICheckInterval
*out = new(metav1.Duration)
**out = **in
}
if in.PatroniAPICheckTimeout != nil {
in, out := &in.PatroniAPICheckTimeout, &out.PatroniAPICheckTimeout
*out = new(metav1.Duration)
**out = **in
}
return
}
@ -795,16 +858,6 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) {
*out = new(int32)
**out = **in
}
if in.UseLoadBalancer != nil {
in, out := &in.UseLoadBalancer, &out.UseLoadBalancer
*out = new(bool)
**out = **in
}
if in.ReplicaLoadBalancer != nil {
in, out := &in.ReplicaLoadBalancer, &out.ReplicaLoadBalancer
*out = new(bool)
**out = **in
}
if in.AllowedSourceRanges != nil {
in, out := &in.AllowedSourceRanges, &out.AllowedSourceRanges
*out = make([]string, len(*in))
@ -973,13 +1026,6 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.InitContainersOld != nil {
in, out := &in.InitContainersOld, &out.InitContainersOld
*out = make([]corev1.Container, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}

View File

@ -1084,7 +1084,7 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
// Patroni service and endpoints / config maps
if err := c.syncPatroniResources(); err != nil {
c.logger.Errorf("could not sync services: %v", err)
c.logger.Errorf("could not sync Patroni resources: %v", err)
updateFailed = true
}
@ -1140,6 +1140,12 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
c.logger.Infof("Storage resize is disabled (storage_resize_mode is off). Skipping volume size sync.")
}
// Pod service account (IRSA annotation sync)
if err := c.syncPodServiceAccount(); err != nil {
c.logger.Errorf("could not sync pod service account: %v", err)
updateFailed = true
}
// Statefulset
func() {
if err := c.syncStatefulSet(); err != nil {

View File

@ -95,6 +95,7 @@ func TestCreate(t *testing.T) {
client := k8sutil.KubernetesClient{
DeploymentsGetter: clientSet.AppsV1(),
ConfigMapsGetter: clientSet.CoreV1(),
CronJobsGetter: clientSet.BatchV1(),
EndpointsGetter: clientSet.CoreV1(),
PersistentVolumeClaimsGetter: clientSet.CoreV1(),
@ -138,7 +139,8 @@ func TestCreate(t *testing.T) {
var cluster = New(
Config{
OpConfig: config.Config{
PodManagementPolicy: "ordered_ready",
PodManagementPolicy: "ordered_ready",
PodTerminateGracePeriod: &metav1.Duration{Duration: 600 * time.Second},
Resources: config.Resources{
ClusterLabels: map[string]string{"application": "spilo"},
ClusterNameLabel: "cluster-name",
@ -147,8 +149,8 @@ func TestCreate(t *testing.T) {
DefaultMemoryRequest: "300Mi",
DefaultMemoryLimit: "300Mi",
PodRoleLabel: "spilo-role",
ResourceCheckInterval: time.Duration(3),
ResourceCheckTimeout: time.Duration(10),
ResourceCheckInterval: &metav1.Duration{Duration: 3 * time.Second},
ResourceCheckTimeout: &metav1.Duration{Duration: 10 * time.Minute},
},
EnableFinalizers: util.True(),
},

View File

@ -337,7 +337,7 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) (
if connectionPoolerSpec == nil {
connectionPoolerSpec = &acidv1.ConnectionPooler{}
}
gracePeriod := int64(c.OpConfig.PodTerminateGracePeriod.Seconds())
gracePeriod := int64(util.CoalesceDuration(c.OpConfig.PodTerminateGracePeriod, "5m").Seconds())
resources, err := c.generateResourceRequirements(
connectionPoolerSpec.Resources,
makeDefaultConnectionPoolerResources(&c.OpConfig),

View File

@ -1165,7 +1165,7 @@ func (c *Cluster) getPodEnvironmentSecretVariables() ([]v1.EnvVar, error) {
secret := &v1.Secret{}
var notFoundErr error
err := retryutil.Retry(c.OpConfig.ResourceCheckInterval, c.OpConfig.ResourceCheckTimeout,
err := retryutil.Retry(c.OpConfig.ResourceCheckInterval.Duration, c.OpConfig.ResourceCheckTimeout.Duration,
func() (bool, error) {
var err error
secret, err = c.KubeClient.Secrets(c.Namespace).Get(
@ -1340,28 +1340,6 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
}
}
// backward compatible check for InitContainers
if spec.InitContainersOld != nil {
msg := "manifest parameter init_containers is deprecated."
if spec.InitContainers == nil {
c.logger.Warningf("%s Consider using initContainers instead.", msg)
spec.InitContainers = spec.InitContainersOld
} else {
c.logger.Warningf("%s Only value from initContainers is used", msg)
}
}
// backward compatible check for PodPriorityClassName
if spec.PodPriorityClassNameOld != "" {
msg := "manifest parameter pod_priority_class_name is deprecated."
if spec.PodPriorityClassName == "" {
c.logger.Warningf("%s Consider using podPriorityClassName instead.", msg)
spec.PodPriorityClassName = spec.PodPriorityClassNameOld
} else {
c.logger.Warningf("%s Only value from podPriorityClassName is used", msg)
}
}
spiloConfiguration, err := generateSpiloJSONConfiguration(&spec.PostgresqlParam, &spec.Patroni, &c.OpConfig, c.logger)
if err != nil {
return nil, fmt.Errorf("could not generate Spilo JSON configuration: %v", err)
@ -1498,7 +1476,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
&tolerationSpec,
c.nodeAffinity(c.OpConfig.NodeReadinessLabel, spec.NodeAffinity),
spec.SchedulerName,
int64(c.OpConfig.PodTerminateGracePeriod.Seconds()),
int64(util.CoalesceDuration(c.OpConfig.PodTerminateGracePeriod, "5m").Seconds()),
c.OpConfig.PodServiceAccountName,
c.OpConfig.KubeIAMRole,
effectivePodPriorityClassName,
@ -2300,6 +2278,12 @@ func (c *Cluster) generatePrimaryPodDisruptionBudget() *policyv1.PodDisruptionBu
labels[c.OpConfig.PodRoleLabel] = string(Master)
}
// When master selector is disabled and synchronous_mode_strict is on, require
// master + synchronous_node_count (default 1) healthy pods for write quorum.
if pdbMasterLabelSelector != nil && !*pdbMasterLabelSelector && minAvailable.IntVal > 0 && c.Spec.SynchronousModeStrict {
minAvailable = intstr.FromInt32(int32(c.Spec.SynchronousNodeCount + 1))
}
return &policyv1.PodDisruptionBudget{
ObjectMeta: metav1.ObjectMeta{
Name: c.PrimaryPodDisruptionBudgetName(),
@ -2431,7 +2415,7 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) {
&tolerationsSpec,
c.nodeAffinity(c.OpConfig.NodeReadinessLabel, nil),
nil,
int64(c.OpConfig.PodTerminateGracePeriod.Seconds()),
int64(util.CoalesceDuration(c.OpConfig.PodTerminateGracePeriod, "5m").Seconds()),
c.OpConfig.PodServiceAccountName,
c.OpConfig.KubeIAMRole,
"",

View File

@ -6,7 +6,6 @@ import (
"reflect"
"sort"
"testing"
"time"
"github.com/stretchr/testify/assert"
@ -361,8 +360,8 @@ func TestPodEnvironmentSecretVariables(t *testing.T) {
opConfig: config.Config{
Resources: config.Resources{
PodEnvironmentSecret: testPodEnvironmentObjectNotExists,
ResourceCheckInterval: time.Duration(testResourceCheckInterval),
ResourceCheckTimeout: time.Duration(testResourceCheckTimeout),
ResourceCheckInterval: &metav1.Duration{Duration: testResourceCheckInterval},
ResourceCheckTimeout: &metav1.Duration{Duration: testResourceCheckTimeout},
},
},
err: fmt.Errorf("could not read Secret PodEnvironmentSecretName: still failing after %d retries: secret.core %q not found", maxRetries, testPodEnvironmentObjectNotExists),
@ -372,8 +371,8 @@ func TestPodEnvironmentSecretVariables(t *testing.T) {
opConfig: config.Config{
Resources: config.Resources{
PodEnvironmentSecret: testPodEnvironmentSecretNameAPIError,
ResourceCheckInterval: time.Duration(testResourceCheckInterval),
ResourceCheckTimeout: time.Duration(testResourceCheckTimeout),
ResourceCheckInterval: &metav1.Duration{Duration: testResourceCheckInterval},
ResourceCheckTimeout: &metav1.Duration{Duration: testResourceCheckTimeout},
},
},
err: fmt.Errorf("could not read Secret PodEnvironmentSecretName: Secret PodEnvironmentSecret API error"),
@ -383,8 +382,8 @@ func TestPodEnvironmentSecretVariables(t *testing.T) {
opConfig: config.Config{
Resources: config.Resources{
PodEnvironmentSecret: testPodEnvironmentSecretName,
ResourceCheckInterval: time.Duration(testResourceCheckInterval),
ResourceCheckTimeout: time.Duration(testResourceCheckTimeout),
ResourceCheckInterval: &metav1.Duration{Duration: testResourceCheckInterval},
ResourceCheckTimeout: &metav1.Duration{Duration: testResourceCheckTimeout},
},
},
envVars: []v1.EnvVar{
@ -583,60 +582,60 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) {
}
expectedValuesS3Bucket := []ExpectedValue{
{
envIndex: 15,
envIndex: 16,
envVarConstant: "WAL_S3_BUCKET",
envVarValue: "global-s3-bucket",
},
{
envIndex: 16,
envIndex: 17,
envVarConstant: "WAL_BUCKET_SCOPE_SUFFIX",
envVarValue: fmt.Sprintf("/%s", dummyUUID),
},
{
envIndex: 17,
envIndex: 18,
envVarConstant: "WAL_BUCKET_SCOPE_PREFIX",
envVarValue: "",
},
}
expectedValuesGCPCreds := []ExpectedValue{
{
envIndex: 15,
envIndex: 16,
envVarConstant: "WAL_GS_BUCKET",
envVarValue: "global-gs-bucket",
},
{
envIndex: 16,
envIndex: 17,
envVarConstant: "WAL_BUCKET_SCOPE_SUFFIX",
envVarValue: fmt.Sprintf("/%s", dummyUUID),
},
{
envIndex: 17,
envIndex: 18,
envVarConstant: "WAL_BUCKET_SCOPE_PREFIX",
envVarValue: "",
},
{
envIndex: 18,
envIndex: 19,
envVarConstant: "GOOGLE_APPLICATION_CREDENTIALS",
envVarValue: "some-path-to-credentials",
},
}
expectedS3BucketConfigMap := []ExpectedValue{
{
envIndex: 17,
envIndex: 18,
envVarConstant: "wal_s3_bucket",
envVarValue: "global-s3-bucket-configmap",
},
}
expectedCustomS3BucketSpec := []ExpectedValue{
{
envIndex: 15,
envIndex: 16,
envVarConstant: "WAL_S3_BUCKET",
envVarValue: "custom-s3-bucket",
},
}
expectedCustomVariableSecret := []ExpectedValue{
{
envIndex: 16,
envIndex: 17,
envVarConstant: "custom_variable",
envVarValueRef: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{
@ -650,72 +649,72 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) {
}
expectedCustomVariableConfigMap := []ExpectedValue{
{
envIndex: 16,
envIndex: 17,
envVarConstant: "custom_variable",
envVarValue: "configmap-test",
},
}
expectedCustomVariableSpec := []ExpectedValue{
{
envIndex: 15,
envIndex: 16,
envVarConstant: "CUSTOM_VARIABLE",
envVarValue: "spec-env-test",
},
}
expectedCloneEnvSpec := []ExpectedValue{
{
envIndex: 16,
envIndex: 17,
envVarConstant: "CLONE_WALE_S3_PREFIX",
envVarValue: "s3://another-bucket",
},
{
envIndex: 19,
envIndex: 20,
envVarConstant: "CLONE_WAL_BUCKET_SCOPE_PREFIX",
envVarValue: "",
},
{
envIndex: 20,
envIndex: 21,
envVarConstant: "CLONE_AWS_ENDPOINT",
envVarValue: "s3.eu-central-1.amazonaws.com",
},
}
expectedCloneEnvSpecEnv := []ExpectedValue{
{
envIndex: 15,
envIndex: 16,
envVarConstant: "CLONE_WAL_BUCKET_SCOPE_PREFIX",
envVarValue: "test-cluster",
},
{
envIndex: 17,
envIndex: 18,
envVarConstant: "CLONE_WALE_S3_PREFIX",
envVarValue: "s3://another-bucket",
},
{
envIndex: 21,
envIndex: 22,
envVarConstant: "CLONE_AWS_ENDPOINT",
envVarValue: "s3.eu-central-1.amazonaws.com",
},
}
expectedCloneEnvConfigMap := []ExpectedValue{
{
envIndex: 16,
envIndex: 17,
envVarConstant: "CLONE_WAL_S3_BUCKET",
envVarValue: "global-s3-bucket",
},
{
envIndex: 17,
envIndex: 18,
envVarConstant: "CLONE_WAL_BUCKET_SCOPE_SUFFIX",
envVarValue: fmt.Sprintf("/%s", dummyUUID),
},
{
envIndex: 21,
envIndex: 22,
envVarConstant: "clone_aws_endpoint",
envVarValue: "s3.eu-west-1.amazonaws.com",
},
}
expectedCloneEnvSecret := []ExpectedValue{
{
envIndex: 21,
envIndex: 22,
envVarConstant: "clone_aws_access_key_id",
envVarValueRef: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{
@ -729,12 +728,12 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) {
}
expectedStandbyEnvSecret := []ExpectedValue{
{
envIndex: 15,
envIndex: 16,
envVarConstant: "STANDBY_WALE_GS_PREFIX",
envVarValue: "gs://some/path/",
},
{
envIndex: 20,
envIndex: 21,
envVarConstant: "standby_google_application_credentials",
envVarValueRef: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{
@ -848,8 +847,8 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) {
Name: testPodEnvironmentConfigMapName,
},
PodEnvironmentSecret: testPodEnvironmentSecretName,
ResourceCheckInterval: time.Duration(testResourceCheckInterval),
ResourceCheckTimeout: time.Duration(testResourceCheckTimeout),
ResourceCheckInterval: &metav1.Duration{Duration: testResourceCheckInterval},
ResourceCheckTimeout: &metav1.Duration{Duration: testResourceCheckTimeout},
},
},
cloneDescription: &acidv1.CloneDescription{},
@ -877,8 +876,8 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) {
Name: testPodEnvironmentConfigMapName,
},
PodEnvironmentSecret: testPodEnvironmentSecretName,
ResourceCheckInterval: time.Duration(testResourceCheckInterval),
ResourceCheckTimeout: time.Duration(testResourceCheckTimeout),
ResourceCheckInterval: &metav1.Duration{Duration: testResourceCheckInterval},
ResourceCheckTimeout: &metav1.Duration{Duration: testResourceCheckTimeout},
},
},
cloneDescription: &acidv1.CloneDescription{},
@ -968,8 +967,8 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) {
opConfig: config.Config{
Resources: config.Resources{
PodEnvironmentSecret: testPodEnvironmentSecretName,
ResourceCheckInterval: time.Duration(testResourceCheckInterval),
ResourceCheckTimeout: time.Duration(testResourceCheckTimeout),
ResourceCheckInterval: &metav1.Duration{Duration: testResourceCheckInterval},
ResourceCheckTimeout: &metav1.Duration{Duration: testResourceCheckTimeout},
},
WALES3Bucket: "global-s3-bucket",
},
@ -986,8 +985,8 @@ func TestGenerateSpiloPodEnvVars(t *testing.T) {
opConfig: config.Config{
Resources: config.Resources{
PodEnvironmentSecret: testPodEnvironmentSecretName,
ResourceCheckInterval: time.Duration(testResourceCheckInterval),
ResourceCheckTimeout: time.Duration(testResourceCheckTimeout),
ResourceCheckInterval: &metav1.Duration{Duration: testResourceCheckInterval},
ResourceCheckTimeout: &metav1.Duration{Duration: testResourceCheckTimeout},
},
WALES3Bucket: "global-s3-bucket",
},
@ -2692,13 +2691,13 @@ func TestGeneratePodDisruptionBudget(t *testing.T) {
k8sutil.KubernetesClient{},
acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{Name: "myapp-database", Namespace: "myapp"},
Spec: acidv1.PostgresSpec{TeamID: "myapp", NumberOfInstances: 3}},
Spec: acidv1.PostgresSpec{TeamID: "myapp", NumberOfInstances: 3, Patroni: acidv1.Patroni{SynchronousModeStrict: true, SynchronousNodeCount: 1}}},
logger,
eventRecorder),
check: []func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error{
testPodDisruptionBudgetOwnerReference,
hasName("postgres-myapp-database-pdb"),
hasMinAvailable(1),
hasMinAvailable(2),
testLabelsAndSelectors(true),
},
},
@ -2977,6 +2976,7 @@ func getServices(serviceType v1.ServiceType, sourceRanges []string, extTrafficPo
ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyType(extTrafficPolicy),
LoadBalancerSourceRanges: sourceRanges,
Ports: []v1.ServicePort{{Name: "postgresql", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}, NodePort: nodePort}},
Selector: map[string]string{"spilo-role": "master", "application": "spilo", "cluster-name": clusterName},
Type: serviceType,
},
{

View File

@ -334,7 +334,7 @@ func (c *Cluster) getPatroniConfig(pod *v1.Pod) (acidv1.Patroni, map[string]stri
pgParameters map[string]string
)
podName := util.NameFromMeta(pod.ObjectMeta)
err := retryutil.Retry(c.OpConfig.PatroniAPICheckInterval, c.OpConfig.PatroniAPICheckTimeout,
err := retryutil.Retry(c.OpConfig.PatroniAPICheckInterval.Duration, c.OpConfig.PatroniAPICheckTimeout.Duration,
func() (bool, error) {
var err error
patroniConfig, pgParameters, err = c.patroni.GetConfig(pod)
@ -355,7 +355,7 @@ func (c *Cluster) getPatroniConfig(pod *v1.Pod) (acidv1.Patroni, map[string]stri
func (c *Cluster) getPatroniMemberData(pod *v1.Pod) (patroni.MemberData, error) {
var memberData patroni.MemberData
err := retryutil.Retry(c.OpConfig.PatroniAPICheckInterval, c.OpConfig.PatroniAPICheckTimeout,
err := retryutil.Retry(c.OpConfig.PatroniAPICheckInterval.Duration, c.OpConfig.PatroniAPICheckTimeout.Duration,
func() (bool, error) {
var err error
memberData, err = c.patroni.GetMemberData(pod)
@ -506,7 +506,7 @@ func (c *Cluster) getSwitchoverCandidate(master *v1.Pod) (spec.NamespacedName, e
candidates := make([]patroni.ClusterMember, 0)
syncCandidates := make([]patroni.ClusterMember, 0)
err := retryutil.Retry(c.OpConfig.PatroniAPICheckInterval, c.OpConfig.PatroniAPICheckTimeout,
err := retryutil.Retry(c.OpConfig.PatroniAPICheckInterval.Duration, c.OpConfig.PatroniAPICheckTimeout.Duration,
func() (bool, error) {
var err error
members, err = c.patroni.GetClusterMembers(master)

View File

@ -16,6 +16,7 @@ import (
"github.com/zalando/postgres-operator/pkg/util/k8sutil"
"github.com/zalando/postgres-operator/pkg/util/patroni"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestGetSwitchoverCandidate(t *testing.T) {
@ -28,8 +29,8 @@ func TestGetSwitchoverCandidate(t *testing.T) {
var cluster = New(
Config{
OpConfig: config.Config{
PatroniAPICheckInterval: time.Duration(1),
PatroniAPICheckTimeout: time.Duration(5),
PatroniAPICheckInterval: &metav1.Duration{Duration: 1 * time.Second},
PatroniAPICheckTimeout: &metav1.Duration{Duration: 5 * time.Second},
},
}, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder)

View File

@ -234,7 +234,7 @@ func (c *Cluster) replaceStatefulSet(newStatefulSet *appsv1.StatefulSet) error {
// wait until the statefulset is truly deleted
c.logger.Debug("waiting for the statefulset to be deleted")
err = retryutil.Retry(c.OpConfig.ResourceCheckInterval, c.OpConfig.ResourceCheckTimeout,
err = retryutil.Retry(c.OpConfig.ResourceCheckInterval.Duration, c.OpConfig.ResourceCheckTimeout.Duration,
func() (bool, error) {
_, err2 := c.KubeClient.StatefulSets(oldStatefulset.Namespace).Get(context.TODO(), oldStatefulset.Name, metav1.GetOptions{})
if err2 == nil {
@ -550,7 +550,7 @@ func (c *Cluster) deletePrimaryPodDisruptionBudget() error {
c.logger.Infof("pod disruption budget %q has been deleted", util.NameFromMeta(c.PrimaryPodDisruptionBudget.ObjectMeta))
c.PrimaryPodDisruptionBudget = nil
err = retryutil.Retry(c.OpConfig.ResourceCheckInterval, c.OpConfig.ResourceCheckTimeout,
err = retryutil.Retry(c.OpConfig.ResourceCheckInterval.Duration, c.OpConfig.ResourceCheckTimeout.Duration,
func() (bool, error) {
_, err2 := c.KubeClient.PodDisruptionBudgets(pdbName.Namespace).Get(context.TODO(), pdbName.Name, metav1.GetOptions{})
if err2 == nil {
@ -588,7 +588,7 @@ func (c *Cluster) deleteCriticalOpPodDisruptionBudget() error {
c.logger.Infof("pod disruption budget %q has been deleted", util.NameFromMeta(c.CriticalOpPodDisruptionBudget.ObjectMeta))
c.CriticalOpPodDisruptionBudget = nil
err = retryutil.Retry(c.OpConfig.ResourceCheckInterval, c.OpConfig.ResourceCheckTimeout,
err = retryutil.Retry(c.OpConfig.ResourceCheckInterval.Duration, c.OpConfig.ResourceCheckTimeout.Duration,
func() (bool, error) {
_, err2 := c.KubeClient.PodDisruptionBudgets(pdbName.Namespace).Get(context.TODO(), pdbName.Name, metav1.GetOptions{})
if err2 == nil {

View File

@ -67,6 +67,10 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error {
}
}()
if !c.patroniKubernetesUseConfigMaps() {
c.logger.Warning("K8s endpoints are deprecated. Please, enable kubernetes_use_configmaps. Requires scale-in to a single primary, see v1 -> v2 migration docs!")
}
if err = c.syncFinalizer(); err != nil {
c.logger.Debugf("could not sync finalizers: %v", err)
}
@ -103,6 +107,10 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error {
}
}
if err = c.syncPodServiceAccount(); err != nil {
c.logger.Errorf("could not sync pod service account: %v", err)
}
if err = c.syncStatefulSet(); err != nil {
if !k8sutil.ResourceAlreadyExists(err) {
err = fmt.Errorf("could not sync statefulsets: %v", err)
@ -626,6 +634,10 @@ func (c *Cluster) syncStatefulSet() error {
if !cmp.rollingUpdate {
updatedPodAnnotations := map[string]*string{}
for _, anno := range cmp.deletedPodAnnotations {
// during IRSA migration let kube2iam annotation drain naturally via pod rotation
if c.OpConfig.IRSARoleARN != "" && anno == constants.KubeIAmAnnotation {
continue
}
updatedPodAnnotations[anno] = nil
}
for anno, val := range desiredSts.Spec.Template.Annotations {
@ -1799,6 +1811,7 @@ func (c *Cluster) syncLogicalBackupJob() error {
// no existing logical backup job, create new one
c.logger.Info("could not find the cluster's logical backup job")
if err = c.createLogicalBackupJob(); err == nil {
c.logger.Infof("created missing logical backup job %s", jobName)
} else {
@ -1813,3 +1826,62 @@ func (c *Cluster) syncLogicalBackupJob() error {
return nil
}
func (c *Cluster) syncPodServiceAccount() error {
sa, err := c.KubeClient.ServiceAccounts(c.Namespace).Get(context.TODO(), c.OpConfig.PodServiceAccountName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("could not get pod service account %q: %v", c.OpConfig.PodServiceAccountName, err)
}
changed := false
if c.OpConfig.IRSARoleARN != "" {
if val, ok := sa.Annotations[constants.IRSAAnnotation]; !ok || val != c.OpConfig.IRSARoleARN {
if sa.Annotations == nil {
sa.Annotations = make(map[string]string)
}
sa.Annotations[constants.IRSAAnnotation] = c.OpConfig.IRSARoleARN
changed = true
}
} else {
if _, ok := sa.Annotations[constants.IRSAAnnotation]; ok {
delete(sa.Annotations, constants.IRSAAnnotation)
changed = true
}
}
if changed {
if _, err = c.KubeClient.ServiceAccounts(c.Namespace).Update(context.TODO(), sa, metav1.UpdateOptions{}); err != nil {
return fmt.Errorf("could not update pod service account %q: %v", sa.Name, err)
}
c.logger.Infof("synced annotations on pod service account %q", sa.Name)
}
if c.OpConfig.IRSARoleARN != "" {
c.logIRSAMigrationProgress()
}
return nil
}
func (c *Cluster) logIRSAMigrationProgress() {
pods, err := c.listPods()
if err != nil {
c.logger.Warnf("IRSA migration: could not list pods: %v", err)
return
}
total := len(pods)
remaining := 0
for _, pod := range pods {
if _, ok := pod.Annotations[constants.KubeIAmAnnotation]; ok {
remaining++
}
}
if remaining > 0 {
c.logger.Infof("IRSA migration in progress: %d/%d pods still carry kube2iam annotation, will be removed on next rotation", remaining, total)
} else {
c.logger.Infof("IRSA migration complete: all %d pods have rotated, kube2iam annotation fully drained", total)
}
}

View File

@ -101,8 +101,8 @@ func TestSyncStatefulSetsAnnotations(t *testing.T) {
DefaultMemoryLimit: "300Mi",
InheritedAnnotations: []string{inheritedAnnotation},
PodRoleLabel: "spilo-role",
ResourceCheckInterval: time.Duration(3),
ResourceCheckTimeout: time.Duration(10),
ResourceCheckInterval: &metav1.Duration{Duration: 3 * time.Second},
ResourceCheckTimeout: &metav1.Duration{Duration: 10 * time.Minute},
},
},
}, client, pg, logger, eventRecorder)
@ -187,8 +187,8 @@ func TestPodAnnotationsSync(t *testing.T) {
var cluster = New(
Config{
OpConfig: config.Config{
PatroniAPICheckInterval: time.Duration(1),
PatroniAPICheckTimeout: time.Duration(5),
PatroniAPICheckInterval: &metav1.Duration{Duration: 1 * time.Second},
PatroniAPICheckTimeout: &metav1.Duration{Duration: 5 * time.Second},
PodManagementPolicy: "ordered_ready",
CustomPodAnnotations: customPodAnnotations,
ConnectionPooler: config.ConnectionPooler{
@ -207,8 +207,8 @@ func TestPodAnnotationsSync(t *testing.T) {
DefaultMemoryLimit: "300Mi",
MaxInstances: -1,
PodRoleLabel: "spilo-role",
ResourceCheckInterval: time.Duration(3),
ResourceCheckTimeout: time.Duration(10),
ResourceCheckInterval: &metav1.Duration{Duration: 3 * time.Second},
ResourceCheckTimeout: &metav1.Duration{Duration: 10 * time.Minute},
},
},
}, client, pg, logger, eventRecorder)
@ -381,8 +381,8 @@ func TestCheckAndSetGlobalPostgreSQLConfiguration(t *testing.T) {
DefaultMemoryRequest: "300Mi",
DefaultMemoryLimit: "300Mi",
PodRoleLabel: "spilo-role",
ResourceCheckInterval: time.Duration(3),
ResourceCheckTimeout: time.Duration(10),
ResourceCheckInterval: &metav1.Duration{Duration: 3 * time.Second},
ResourceCheckTimeout: &metav1.Duration{Duration: 10 * time.Minute},
},
},
}, client, pg, logger, eventRecorder)
@ -694,8 +694,8 @@ func TestSyncStandbyClusterConfiguration(t *testing.T) {
var cluster = New(
Config{
OpConfig: config.Config{
PatroniAPICheckInterval: time.Duration(1),
PatroniAPICheckTimeout: time.Duration(5),
PatroniAPICheckInterval: &metav1.Duration{Duration: 1 * time.Second},
PatroniAPICheckTimeout: &metav1.Duration{Duration: 5 * time.Second},
PodManagementPolicy: "ordered_ready",
Resources: config.Resources{
ClusterLabels: map[string]string{"application": applicationLabel},
@ -707,8 +707,8 @@ func TestSyncStandbyClusterConfiguration(t *testing.T) {
MinInstances: int32(-1),
MaxInstances: int32(-1),
PodRoleLabel: "spilo-role",
ResourceCheckInterval: time.Duration(3),
ResourceCheckTimeout: time.Duration(10),
ResourceCheckInterval: &metav1.Duration{Duration: 3 * time.Second},
ResourceCheckTimeout: &metav1.Duration{Duration: 10 * time.Minute},
},
},
}, client, pg, logger, eventRecorder)

View File

@ -342,7 +342,7 @@ func (c *Cluster) annotationsSet(annotations map[string]string) map[string]strin
}
func (c *Cluster) waitForPodLabel(podEvents chan PodEvent, stopCh chan struct{}, role *PostgresRole) (*v1.Pod, error) {
timeout := time.After(c.OpConfig.PodLabelWaitTimeout)
timeout := time.After(c.OpConfig.PodLabelWaitTimeout.Duration)
for {
select {
case podEvent := <-podEvents:
@ -364,7 +364,7 @@ func (c *Cluster) waitForPodLabel(podEvents chan PodEvent, stopCh chan struct{},
}
func (c *Cluster) waitForPodDeletion(podEvents chan PodEvent) error {
timeout := time.After(c.OpConfig.PodDeletionWaitTimeout)
timeout := time.After(c.OpConfig.PodDeletionWaitTimeout.Duration)
for {
select {
case podEvent := <-podEvents:
@ -378,7 +378,7 @@ func (c *Cluster) waitForPodDeletion(podEvents chan PodEvent) error {
}
func (c *Cluster) waitStatefulsetReady() error {
return retryutil.Retry(c.OpConfig.ResourceCheckInterval, c.OpConfig.ResourceCheckTimeout,
return retryutil.Retry(c.OpConfig.ResourceCheckInterval.Duration, c.OpConfig.ResourceCheckTimeout.Duration,
func() (bool, error) {
listOptions := metav1.ListOptions{
LabelSelector: c.labelsSet(false).String(),
@ -428,7 +428,7 @@ func (c *Cluster) _waitPodLabelsReady(anyReplica bool) error {
c.logger.Debug("Waiting for any replica pod to become ready")
}
err := retryutil.Retry(c.OpConfig.ResourceCheckInterval, c.OpConfig.ResourceCheckTimeout,
err := retryutil.Retry(c.OpConfig.ResourceCheckInterval.Duration, c.OpConfig.ResourceCheckTimeout.Duration,
func() (bool, error) {
masterCount := 0
if !anyReplica {
@ -627,9 +627,12 @@ func (c *Cluster) patroniKubernetesUseConfigMaps() bool {
if !c.patroniUsesKubernetes() {
return false
}
if c.OpConfig.KubernetesUseConfigMaps == nil {
return true
}
// otherwise, follow the operator configuration
return c.OpConfig.KubernetesUseConfigMaps
return *c.OpConfig.KubernetesUseConfigMaps
}
// Earlier arguments take priority

View File

@ -53,6 +53,7 @@ func newFakeK8sAnnotationsClient() (k8sutil.KubernetesClient, *k8sFake.Clientset
EndpointsGetter: clientSet.CoreV1(),
ConfigMapsGetter: clientSet.CoreV1(),
PodsGetter: clientSet.CoreV1(),
ServiceAccountsGetter: clientSet.CoreV1(),
DeploymentsGetter: clientSet.AppsV1(),
CronJobsGetter: clientSet.BatchV1(),
}, clientSet
@ -297,9 +298,9 @@ func newInheritedAnnotationsCluster(client k8sutil.KubernetesClient) (*Cluster,
cluster := New(
Config{
OpConfig: config.Config{
PatroniAPICheckInterval: time.Duration(1),
PatroniAPICheckTimeout: time.Duration(5),
KubernetesUseConfigMaps: true,
PatroniAPICheckInterval: &metav1.Duration{Duration: 1 * time.Second},
PatroniAPICheckTimeout: &metav1.Duration{Duration: 5 * time.Second},
KubernetesUseConfigMaps: util.True(),
ConnectionPooler: config.ConnectionPooler{
ConnectionPoolerDefaultCPURequest: "100m",
ConnectionPoolerDefaultCPULimit: "100m",
@ -318,8 +319,8 @@ func newInheritedAnnotationsCluster(client k8sutil.KubernetesClient) (*Cluster,
DefaultMemoryLimit: "300Mi",
InheritedAnnotations: []string{"owned-by"},
PodRoleLabel: "spilo-role",
ResourceCheckInterval: time.Duration(testResourceCheckInterval),
ResourceCheckTimeout: time.Duration(testResourceCheckTimeout),
ResourceCheckInterval: &metav1.Duration{Duration: testResourceCheckInterval},
ResourceCheckTimeout: &metav1.Duration{Duration: testResourceCheckTimeout},
MinInstances: -1,
MaxInstances: -1,
},
@ -388,7 +389,7 @@ func createPatroniResources(cluster *Cluster) error {
Labels: cluster.labelsSet(false),
}
if cluster.OpConfig.KubernetesUseConfigMaps {
if cluster.OpConfig.KubernetesUseConfigMaps != nil && *cluster.OpConfig.KubernetesUseConfigMaps {
configMap := v1.ConfigMap{
ObjectMeta: metadata,
}
@ -598,7 +599,7 @@ func TestInheritedAnnotations(t *testing.T) {
// 3. Change from ConfigMaps to Endpoints
err = cluster.deletePatroniResources()
assert.NoError(t, err)
cluster.OpConfig.KubernetesUseConfigMaps = false
cluster.OpConfig.KubernetesUseConfigMaps = util.False()
err = createPatroniResources(cluster)
assert.NoError(t, err)
err = cluster.Sync(newSpec.DeepCopy())

View File

@ -247,6 +247,12 @@ func (c *Controller) initPodServiceAccount() {
c.PodServiceAccount.Name = c.opConfig.PodServiceAccountName
}
c.PodServiceAccount.Namespace = ""
if c.opConfig.IRSARoleARN != "" {
if c.PodServiceAccount.Annotations == nil {
c.PodServiceAccount.Annotations = make(map[string]string)
}
c.PodServiceAccount.Annotations[constants.IRSAAnnotation] = c.opConfig.IRSARoleARN
}
}
// actual service accounts are deployed at the time of Postgres/Spilo cluster creation

View File

@ -152,7 +152,7 @@ func (c *Controller) nodeDelete(obj interface{}) {
func (c *Controller) moveMasterPodsOffNode(node *v1.Node) {
// retry to move master until configured timeout is reached
err := retryutil.Retry(1*time.Minute, c.opConfig.MasterPodMoveTimeout,
err := retryutil.Retry(1*time.Minute, c.opConfig.MasterPodMoveTimeout.Duration,
func() (bool, error) {
err := c.attemptToMoveMasterPodsOffNode(node)
if err != nil {

View File

@ -4,8 +4,6 @@ import (
"context"
"fmt"
"time"
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
"github.com/zalando/postgres-operator/pkg/util"
"github.com/zalando/postgres-operator/pkg/util/config"
@ -37,30 +35,21 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.EnableSpiloWalPathCompat = fromCRD.EnableSpiloWalPathCompat
result.EnableTeamIdClusternamePrefix = fromCRD.EnableTeamIdClusternamePrefix
result.EtcdHost = fromCRD.EtcdHost
result.KubernetesUseConfigMaps = fromCRD.KubernetesUseConfigMaps
result.KubernetesUseConfigMaps = util.CoalesceBool(fromCRD.KubernetesUseConfigMaps, util.True())
result.DockerImage = util.Coalesce(fromCRD.DockerImage, "ghcr.io/zalando/spilo-18:4.1-p1")
result.Workers = util.CoalesceUInt32(fromCRD.Workers, 8)
result.MinInstances = fromCRD.MinInstances
result.MaxInstances = fromCRD.MaxInstances
result.IgnoreInstanceLimitsAnnotationKey = fromCRD.IgnoreInstanceLimitsAnnotationKey
result.IgnoreResourcesLimitsAnnotationKey = fromCRD.IgnoreResourcesLimitsAnnotationKey
result.ResyncPeriod = util.CoalesceDuration(time.Duration(fromCRD.ResyncPeriod), "30m")
result.RepairPeriod = util.CoalesceDuration(time.Duration(fromCRD.RepairPeriod), "5m")
result.ResyncPeriod = util.CoalesceDuration(fromCRD.ResyncPeriod, "30m")
result.RepairPeriod = util.CoalesceDuration(fromCRD.RepairPeriod, "5m")
result.SetMemoryRequestToLimit = fromCRD.SetMemoryRequestToLimit
result.ShmVolume = util.CoalesceBool(fromCRD.ShmVolume, util.True())
result.SidecarImages = fromCRD.SidecarImages
result.SidecarContainers = fromCRD.SidecarContainers
result.EnableMaintenanceWindows = util.CoalesceBool(fromCRD.EnableMaintenanceWindows, util.True())
if len(fromCRD.MaintenanceWindows) > 0 {
result.MaintenanceWindows = make([]string, 0, len(fromCRD.MaintenanceWindows))
for _, window := range fromCRD.MaintenanceWindows {
w, err := window.MarshalJSON()
if err != nil {
panic(fmt.Errorf("could not marshal configured maintenance window: %v", err))
}
result.MaintenanceWindows = append(result.MaintenanceWindows, string(w))
}
}
result.MaintenanceWindows = fromCRD.MaintenanceWindows
// user config
result.SuperUsername = util.Coalesce(fromCRD.PostgresUsersConfiguration.SuperUsername, "postgres")
@ -84,7 +73,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.PodServiceAccountRoleBindingDefinition = fromCRD.Kubernetes.PodServiceAccountRoleBindingDefinition
result.PodEnvironmentConfigMap = fromCRD.Kubernetes.PodEnvironmentConfigMap
result.PodEnvironmentSecret = fromCRD.Kubernetes.PodEnvironmentSecret
result.PodTerminateGracePeriod = util.CoalesceDuration(time.Duration(fromCRD.Kubernetes.PodTerminateGracePeriod), "5m")
result.PodTerminateGracePeriod = util.CoalesceDuration(fromCRD.Kubernetes.PodTerminateGracePeriod, "5m")
result.LivenessProbe = fromCRD.Kubernetes.LivenessProbe
result.SpiloPrivileged = fromCRD.Kubernetes.SpiloPrivileged
result.SpiloAllowPrivilegeEscalation = util.CoalesceBool(fromCRD.Kubernetes.SpiloAllowPrivilegeEscalation, util.True())
@ -138,7 +127,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.EnableSecretsDeletion = util.CoalesceBool(fromCRD.Kubernetes.EnableSecretsDeletion, util.True())
result.EnablePersistentVolumeClaimDeletion = util.CoalesceBool(fromCRD.Kubernetes.EnablePersistentVolumeClaimDeletion, util.True())
result.EnableReadinessProbe = fromCRD.Kubernetes.EnableReadinessProbe
result.MasterPodMoveTimeout = util.CoalesceDuration(time.Duration(fromCRD.Kubernetes.MasterPodMoveTimeout), "10m")
result.MasterPodMoveTimeout = util.CoalesceDuration(fromCRD.Kubernetes.MasterPodMoveTimeout, "10m")
result.EnablePodAntiAffinity = fromCRD.Kubernetes.EnablePodAntiAffinity
result.PodAntiAffinityTopologyKey = util.Coalesce(fromCRD.Kubernetes.PodAntiAffinityTopologyKey, "kubernetes.io/hostname")
result.PodAntiAffinityPreferredDuringScheduling = fromCRD.Kubernetes.PodAntiAffinityPreferredDuringScheduling
@ -155,14 +144,14 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.MaxMemoryRequest = fromCRD.PostgresPodResources.MaxMemoryRequest
// timeout config
result.ResourceCheckInterval = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.ResourceCheckInterval), "3s")
result.ResourceCheckTimeout = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.ResourceCheckTimeout), "10m")
result.PodLabelWaitTimeout = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.PodLabelWaitTimeout), "10m")
result.PodDeletionWaitTimeout = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.PodDeletionWaitTimeout), "10m")
result.ReadyWaitInterval = util.CoalesceDuration(time.Duration(fromCRD.Timeouts.ReadyWaitInterval), "4s")
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")
result.ResourceCheckInterval = util.CoalesceDuration(fromCRD.Timeouts.ResourceCheckInterval, "3s")
result.ResourceCheckTimeout = util.CoalesceDuration(fromCRD.Timeouts.ResourceCheckTimeout, "10m")
result.PodLabelWaitTimeout = util.CoalesceDuration(fromCRD.Timeouts.PodLabelWaitTimeout, "10m")
result.PodDeletionWaitTimeout = util.CoalesceDuration(fromCRD.Timeouts.PodDeletionWaitTimeout, "10m")
result.ReadyWaitInterval = util.CoalesceDuration(fromCRD.Timeouts.ReadyWaitInterval, "4s")
result.ReadyWaitTimeout = util.CoalesceDuration(fromCRD.Timeouts.ReadyWaitTimeout, "30s")
result.PatroniAPICheckInterval = util.CoalesceDuration(fromCRD.Timeouts.PatroniAPICheckInterval, "1s")
result.PatroniAPICheckTimeout = util.CoalesceDuration(fromCRD.Timeouts.PatroniAPICheckTimeout, "5s")
// load balancer config
result.DbHostedZone = util.Coalesce(fromCRD.LoadBalancer.DbHostedZone, "db.example.com")
@ -186,6 +175,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.AWSRegion = fromCRD.AWSGCP.AWSRegion
result.LogS3Bucket = fromCRD.AWSGCP.LogS3Bucket
result.KubeIAMRole = fromCRD.AWSGCP.KubeIAMRole
result.IRSARoleARN = fromCRD.AWSGCP.IRSARoleARN
result.WALGSBucket = fromCRD.AWSGCP.WALGSBucket
result.GCPCredentials = fromCRD.AWSGCP.GCPCredentials
result.WALAZStorageAccount = fromCRD.AWSGCP.WALAZStorageAccount

View File

@ -27,7 +27,7 @@ import (
func (c *Controller) clusterResync(stopCh <-chan struct{}, wg *sync.WaitGroup) {
defer wg.Done()
ticker := time.NewTicker(c.opConfig.ResyncPeriod)
ticker := time.NewTicker(c.opConfig.ResyncPeriod.Duration)
for {
select {
@ -213,22 +213,10 @@ func (c *Controller) processEvent(event ClusterEvent, isInInitialList bool) {
}
lg.Debugf("observed cluster status %s, running sync scan to repair the cluster", lastOperationStatus)
event.EventType = EventSync
}
if event.EventType == EventAdd || event.EventType == EventUpdate || event.EventType == EventSync {
// handle deprecated parameters by possibly assigning their values to the new ones.
if event.OldSpec != nil {
c.mergeDeprecatedPostgreSQLSpecParameters(&event.OldSpec.Spec)
}
if event.NewSpec != nil {
c.warnOnDeprecatedPostgreSQLSpecParameters(&event.NewSpec.Spec)
c.mergeDeprecatedPostgreSQLSpecParameters(&event.NewSpec.Spec)
}
} else if event.EventType != EventDelete {
if err = c.submitRBACCredentials(event); err != nil {
c.logger.Warnf("pods and/or Patroni may misfunction due to the lack of permissions: %v", err)
}
}
switch event.EventType {
@ -397,45 +385,6 @@ func (c *Controller) processClusterEventsQueue(idx int, stopCh <-chan struct{},
}
}
func (c *Controller) warnOnDeprecatedPostgreSQLSpecParameters(spec *acidv1.PostgresSpec) {
deprecate := func(deprecated, replacement string) {
c.logger.Warningf("parameter %q is deprecated. Consider setting %q instead", deprecated, replacement)
}
if spec.UseLoadBalancer != nil {
deprecate("useLoadBalancer", "enableMasterLoadBalancer")
}
if spec.ReplicaLoadBalancer != nil {
deprecate("replicaLoadBalancer", "enableReplicaLoadBalancer")
}
if (spec.UseLoadBalancer != nil || spec.ReplicaLoadBalancer != nil) &&
(spec.EnableReplicaLoadBalancer != nil || spec.EnableMasterLoadBalancer != nil) {
c.logger.Warnf("both old and new load balancer parameters are present in the manifest, ignoring old ones")
}
}
// mergeDeprecatedPostgreSQLSpecParameters modifies the spec passed to the cluster by setting current parameter
// values from the obsolete ones. Note: while the spec that is modified is a copy made in queueClusterEvent, it is
// still a shallow copy, so be extra careful not to modify values pointer fields point to, but copy them instead.
func (c *Controller) mergeDeprecatedPostgreSQLSpecParameters(spec *acidv1.PostgresSpec) *acidv1.PostgresSpec {
if (spec.UseLoadBalancer != nil || spec.ReplicaLoadBalancer != nil) &&
(spec.EnableReplicaLoadBalancer == nil && spec.EnableMasterLoadBalancer == nil) {
if spec.UseLoadBalancer != nil {
spec.EnableMasterLoadBalancer = new(bool)
*spec.EnableMasterLoadBalancer = *spec.UseLoadBalancer
}
if spec.ReplicaLoadBalancer != nil {
spec.EnableReplicaLoadBalancer = new(bool)
*spec.EnableReplicaLoadBalancer = *spec.ReplicaLoadBalancer
}
}
spec.ReplicaLoadBalancer = nil
spec.UseLoadBalancer = nil
return spec
}
func (c *Controller) queueClusterEvent(informerOldSpec, informerNewSpec *acidv1.Postgresql, eventType EventType) {
var (
uid types.UID

View File

@ -64,35 +64,6 @@ func TestControllerOwnershipOnPostgresql(t *testing.T) {
}
}
func TestMergeDeprecatedPostgreSQLSpecParameters(t *testing.T) {
tests := []struct {
name string
in *acidv1.PostgresSpec
out *acidv1.PostgresSpec
error string
}{
{
"Check that old parameters propagate values to the new ones",
&acidv1.PostgresSpec{UseLoadBalancer: &True, ReplicaLoadBalancer: &True},
&acidv1.PostgresSpec{UseLoadBalancer: nil, ReplicaLoadBalancer: nil,
EnableMasterLoadBalancer: &True, EnableReplicaLoadBalancer: &True},
"New parameters should be set from the values of old ones",
},
{
"Check that new parameters are not set when both old and new ones are present",
&acidv1.PostgresSpec{UseLoadBalancer: &True, EnableMasterLoadBalancer: &False},
&acidv1.PostgresSpec{UseLoadBalancer: nil, EnableMasterLoadBalancer: &False},
"New parameters should remain unchanged when both old and new are present",
},
}
for _, tt := range tests {
result := postgresqlTestController.mergeDeprecatedPostgreSQLSpecParameters(tt.in)
if !reflect.DeepEqual(result, tt.out) {
t.Errorf("%s: %v", tt.name, tt.error)
}
}
}
func TestMeetsClusterDeleteAnnotations(t *testing.T) {
// set delete annotations in configuration
postgresqlTestController.opConfig.DeleteAnnotationDateKey = "delete-date"

View File

@ -3,33 +3,33 @@ package config
import (
"encoding/json"
"strings"
"time"
"fmt"
"github.com/zalando/postgres-operator/pkg/spec"
"github.com/zalando/postgres-operator/pkg/util/constants"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// CRD describes CustomResourceDefinition specific configuration parameters
type CRD struct {
ReadyWaitInterval time.Duration `name:"ready_wait_interval" default:"4s"`
ReadyWaitTimeout time.Duration `name:"ready_wait_timeout" default:"30s"`
ResyncPeriod time.Duration `name:"resync_period" default:"30m"`
RepairPeriod time.Duration `name:"repair_period" default:"5m"`
EnableCRDRegistration *bool `name:"enable_crd_registration" default:"true"`
CRDCategories []string `name:"crd_categories" default:"all"`
ReadyWaitInterval *metav1.Duration `name:"ready_wait_interval" default:"4s"`
ReadyWaitTimeout *metav1.Duration `name:"ready_wait_timeout" default:"30s"`
ResyncPeriod *metav1.Duration `name:"resync_period" default:"30m"`
RepairPeriod *metav1.Duration `name:"repair_period" default:"5m"`
EnableCRDRegistration *bool `name:"enable_crd_registration" default:"true"`
CRDCategories []string `name:"crd_categories" default:"all"`
}
// Resources describes kubernetes resource specific configuration parameters
type Resources struct {
EnableOwnerReferences *bool `name:"enable_owner_references" default:"false"`
ResourceCheckInterval time.Duration `name:"resource_check_interval" default:"3s"`
ResourceCheckTimeout time.Duration `name:"resource_check_timeout" default:"10m"`
PodLabelWaitTimeout time.Duration `name:"pod_label_wait_timeout" default:"10m"`
PodDeletionWaitTimeout time.Duration `name:"pod_deletion_wait_timeout" default:"10m"`
PodTerminateGracePeriod time.Duration `name:"pod_terminate_grace_period" default:"5m"`
ResourceCheckInterval *metav1.Duration `name:"resource_check_interval" default:"3s"`
ResourceCheckTimeout *metav1.Duration `name:"resource_check_timeout" default:"10m"`
PodLabelWaitTimeout *metav1.Duration `name:"pod_label_wait_timeout" default:"10m"`
PodDeletionWaitTimeout *metav1.Duration `name:"pod_deletion_wait_timeout" default:"10m"`
PodTerminateGracePeriod *metav1.Duration `name:"pod_terminate_grace_period" default:"5m"`
LivenessProbe *v1.Probe `name:"-"`
SpiloRunAsUser *int64 `name:"spilo_runasuser"`
SpiloRunAsGroup *int64 `name:"spilo_runasgroup"`
@ -184,7 +184,7 @@ type Config struct {
ConnectionPooler
WatchedNamespace string `name:"watched_namespace"` // special values: "*" means 'watch all namespaces', the empty string "" means 'watch a namespace where operator is deployed to'
KubernetesUseConfigMaps bool `name:"kubernetes_use_configmaps" default:"false"`
KubernetesUseConfigMaps *bool `name:"kubernetes_use_configmaps" default:"true"`
EtcdHost string `name:"etcd_host" default:""` // special values: the empty string "" means Patroni will use K8s as a DCS
EnableMaintenanceWindows *bool `name:"enable_maintenance_windows" default:"true"`
MaintenanceWindows []string `name:"maintenance_windows"`
@ -195,12 +195,13 @@ type Config struct {
// value of this string must be valid JSON or YAML; see initPodServiceAccount
PodServiceAccountDefinition string `name:"pod_service_account_definition" default:""`
PodServiceAccountRoleBindingDefinition string `name:"pod_service_account_role_binding_definition" default:""`
MasterPodMoveTimeout time.Duration `name:"master_pod_move_timeout" default:"20m"`
MasterPodMoveTimeout *metav1.Duration `name:"master_pod_move_timeout" default:"20m"`
DbHostedZone string `name:"db_hosted_zone" default:"db.example.com"`
AWSRegion string `name:"aws_region" default:"eu-central-1"`
WALES3Bucket string `name:"wal_s3_bucket"`
LogS3Bucket string `name:"log_s3_bucket"`
KubeIAMRole string `name:"kube_iam_role"`
IRSARoleARN string `name:"irsa_role_arn"`
WALGSBucket string `name:"wal_gs_bucket"`
GCPCredentials string `name:"gcp_credentials"`
WALAZStorageAccount string `name:"wal_az_storage_account"`
@ -249,7 +250,7 @@ type Config struct {
RingLogLines int `name:"ring_log_lines" default:"100"`
ClusterHistoryEntries int `name:"cluster_history_entries" default:"1000"`
TeamAPIRoleConfiguration map[string]string `name:"team_api_role_configuration" default:"log_statement:all"`
PodTerminateGracePeriod time.Duration `name:"pod_terminate_grace_period" default:"5m"`
PodTerminateGracePeriod *metav1.Duration `name:"pod_terminate_grace_period" default:"5m"`
PodManagementPolicy string `name:"pod_management_policy" default:"ordered_ready"`
EnableReadinessProbe bool `name:"enable_readiness_probe" default:"false"`
ProtectedRoles []string `name:"protected_role_names" default:"admin,cron_admin"`
@ -265,8 +266,8 @@ type Config struct {
MajorVersionUpgradeTeamAllowList []string `name:"major_version_upgrade_team_allow_list" default:""`
MinimalMajorVersion string `name:"minimal_major_version" default:"14"`
TargetMajorVersion string `name:"target_major_version" default:"18"`
PatroniAPICheckInterval time.Duration `name:"patroni_api_check_interval" default:"1s"`
PatroniAPICheckTimeout time.Duration `name:"patroni_api_check_timeout" default:"5s"`
PatroniAPICheckInterval *metav1.Duration `name:"patroni_api_check_interval" default:"1s"`
PatroniAPICheckTimeout *metav1.Duration `name:"patroni_api_check_timeout" default:"5s"`
EnablePatroniFailsafeMode *bool `name:"enable_patroni_failsafe_mode" default:"false"`
EnableSecretsDeletion *bool `name:"enable_secrets_deletion" default:"true"`
EnablePersistentVolumeClaimDeletion *bool `name:"enable_persistent_volume_claim_deletion" default:"true"`

View File

@ -230,16 +230,16 @@ var newFromMapTests = []struct {
{
description: "duration parsing",
input: map[string]string{
"ready_wait_interval": "10s",
"ready_wait_timeout": "1m",
"patroni_api_check_interval": "1s",
"patroni_api_check_timeout": "5s",
},
expectPanic: false,
validateFunc: func(t *testing.T, cfg *Config) {
if cfg.ReadyWaitInterval.Seconds() != 10 {
t.Errorf("expected ReadyWaitInterval=10s, got %v", cfg.ReadyWaitInterval)
if cfg.PatroniAPICheckInterval.Seconds() != 1 {
t.Errorf("expected check interval of 1s, got %.0fs", cfg.PatroniAPICheckInterval.Seconds())
}
if cfg.ReadyWaitTimeout.Minutes() != 1 {
t.Errorf("expected ReadyWaitTimeout=1m, got %v", cfg.ReadyWaitTimeout)
if cfg.PatroniAPICheckTimeout.Seconds() != 5 {
t.Errorf("expected check timeout of 5s, got %.0fs", cfg.PatroniAPICheckTimeout.Seconds())
}
},
},

View File

@ -7,6 +7,8 @@ import (
"strconv"
"strings"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type decoder interface {
@ -101,13 +103,7 @@ func processField(value string, field reflect.Value) error {
val int64
err error
)
if field.Kind() == reflect.Int64 && typ.PkgPath() == "time" && typ.Name() == "Duration" {
var d time.Duration
d, err = time.ParseDuration(value)
val = int64(d)
} else {
val, err = strconv.ParseInt(value, 0, typ.Bits())
}
val, err = strconv.ParseInt(value, 0, typ.Bits())
if err != nil {
return err
}
@ -165,6 +161,15 @@ func processField(value string, field reflect.Value) error {
mp.SetMapIndex(k, v)
}
field.Set(mp)
case reflect.Struct:
if typ.Name() == "Duration" {
var d time.Duration
d, err := time.ParseDuration(value)
if err != nil {
return err
}
field.Set(reflect.ValueOf(metav1.Duration{Duration: d}))
}
}
return nil

View File

@ -4,6 +4,7 @@ package constants
const (
ZalandoDNSNameAnnotation = "external-dns.alpha.kubernetes.io/hostname"
KubeIAmAnnotation = "iam.amazonaws.com/role"
IRSAAnnotation = "eks.amazonaws.com/role-arn"
VolumeStorateProvisionerAnnotation = "pv.kubernetes.io/provisioned-by"
PostgresqlControllerAnnotationKey = "acid.zalan.do/controller"
)

View File

@ -311,14 +311,14 @@ func CoalesceBool(val, defaultVal *bool) *bool {
return val
}
// CoalesceDuration works like coalesce but for time.Duration
func CoalesceDuration(val time.Duration, defaultVal string) time.Duration {
if val == 0 {
// CoalesceDuration works like coalesce but for metav1.Duration
func CoalesceDuration(val *metav1.Duration, defaultVal string) *metav1.Duration {
if val == nil || val.Duration == 0 {
duration, err := time.ParseDuration(defaultVal)
if err != nil {
panic(err)
}
return duration
return &metav1.Duration{Duration: duration}
}
return val
}