merge with master

This commit is contained in:
Felix Kunde 2019-12-16 12:05:18 +01:00
commit 36bb4e77df
41 changed files with 378 additions and 163 deletions

View File

@ -79,7 +79,8 @@ scm-source.json: .git
tools: tools:
GO111MODULE=on go get -u honnef.co/go/tools/cmd/staticcheck GO111MODULE=on go get -u honnef.co/go/tools/cmd/staticcheck
GO111MODULE=on go get k8s.io/client-go@kubernetes-1.16.0 GO111MODULE=on go get k8s.io/client-go@kubernetes-1.16.3
GO111MODULE=on go mod tidy
fmt: fmt:
@gofmt -l -w -s $(DIRS) @gofmt -l -w -s $(DIRS)

View File

@ -107,10 +107,14 @@ spec:
type: object type: object
additionalProperties: additionalProperties:
type: string type: string
enable_init_containers:
type: boolean
enable_pod_antiaffinity: enable_pod_antiaffinity:
type: boolean type: boolean
enable_pod_disruption_budget: enable_pod_disruption_budget:
type: boolean type: boolean
enable_sidecars:
type: boolean
infrastructure_roles_secret_name: infrastructure_roles_secret_name:
type: string type: string
inherited_labels: inherited_labels:
@ -298,3 +302,7 @@ spec:
pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$'
scalyr_server_url: scalyr_server_url:
type: string type: string
status:
type: object
additionalProperties:
type: string

View File

@ -222,7 +222,7 @@ spec:
# only the format of the given number. # only the format of the given number.
# #
# https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu # https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu
pattern: '^(\d+m|\d+\.\d{1,3})$' pattern: '^(\d+m|\d+(\.\d{1,3})?)$'
# Note: the value specified here must not be zero or be lower # Note: the value specified here must not be zero or be lower
# than the corresponding request. # than the corresponding request.
memory: memory:
@ -253,7 +253,7 @@ spec:
# only the format of the given number. # only the format of the given number.
# #
# https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu # https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu
pattern: '^(\d+m|\d+\.\d{1,3})$' pattern: '^(\d+m|\d+(\.\d{1,3})?)$'
# Note: the value specified here must not be zero or be higher # Note: the value specified here must not be zero or be higher
# than the corresponding limit. # than the corresponding limit.
memory: memory:

View File

@ -59,10 +59,14 @@ configKubernetes:
# annotations attached to each database pod # annotations attached to each database pod
# custom_pod_annotations: keya:valuea,keyb:valueb # custom_pod_annotations: keya:valuea,keyb:valueb
# enables initContainers to run actions before Spilo is started
enable_init_containers: "true"
# toggles pod anti affinity on the Postgres pods # toggles pod anti affinity on the Postgres pods
enable_pod_antiaffinity: "false" enable_pod_antiaffinity: "false"
# toggles PDB to set to MinAvailabe 0 or 1 # toggles PDB to set to MinAvailabe 0 or 1
enable_pod_disruption_budget: "true" enable_pod_disruption_budget: "true"
# enables sidecar containers to run alongside Spilo in the same pod
enable_sidecars: "true"
# name of the secret containing infrastructure roles names and passwords # name of the secret containing infrastructure roles names and passwords
# infrastructure_roles_secret_name: postgresql-infrastructure-roles # infrastructure_roles_secret_name: postgresql-infrastructure-roles

View File

@ -63,10 +63,14 @@ configKubernetes:
# keya: valuea # keya: valuea
# keyb: valueb # keyb: valueb
# enables initContainers to run actions before Spilo is started
enable_init_containers: true
# toggles pod anti affinity on the Postgres pods # toggles pod anti affinity on the Postgres pods
enable_pod_antiaffinity: false enable_pod_antiaffinity: false
# toggles PDB to set to MinAvailabe 0 or 1 # toggles PDB to set to MinAvailabe 0 or 1
enable_pod_disruption_budget: true enable_pod_disruption_budget: true
# enables sidecar containers to run alongside Spilo in the same pod
enable_sidecars: true
# name of the secret containing infrastructure roles names and passwords # name of the secret containing infrastructure roles names and passwords
# infrastructure_roles_secret_name: postgresql-infrastructure-roles # infrastructure_roles_secret_name: postgresql-infrastructure-roles

View File

@ -41,8 +41,8 @@ function aws_upload {
args=() args=()
[[ ! -z "$EXPECTED_SIZE" ]] && args+=("--expected-size=$EXPECTED_SIZE") [[ ! -z "$EXPECTED_SIZE" ]] && args+=("--expected-size=$EXPECTED_SIZE")
[[ ! -z "$LOGICAL_BACKUP_S3_ENDPOINT" ]] && args+=("--endpoint-url=\"$LOGICAL_BACKUP_S3_ENDPOINT\"") [[ ! -z "$LOGICAL_BACKUP_S3_ENDPOINT" ]] && args+=("--endpoint-url=$LOGICAL_BACKUP_S3_ENDPOINT")
[[ ! "$LOGICAL_BACKUP_S3_SSE" == "" ]] && args+=("--sse=\"$LOGICAL_BACKUP_S3_SSE\"") [[ ! "$LOGICAL_BACKUP_S3_SSE" == "" ]] && args+=("--sse=$LOGICAL_BACKUP_S3_SSE")
aws s3 cp - "$PATH_TO_BACKUP" "${args[@]//\'/}" --debug aws s3 cp - "$PATH_TO_BACKUP" "${args[@]//\'/}" --debug
} }

View File

@ -3,6 +3,26 @@
Learn how to configure and manage the Postgres Operator in your Kubernetes (K8s) Learn how to configure and manage the Postgres Operator in your Kubernetes (K8s)
environment. environment.
## Minor and major version upgrade
Minor version upgrades for PostgreSQL are handled via updating the Spilo Docker
image. The operator will carry out a rolling update of Pods which includes a
switchover (planned failover) of the master to the Pod with new minor version.
The switch should usually take less than 5 seconds, still clients have to
reconnect.
Major version upgrades are supported via [cloning](user.md#clone-directly). The
new cluster manifest must have a higher `version` string than the source cluster
and will be created from a basebackup. Depending of the cluster size, downtime
in this case can be significant as writes to the database should be stopped and
all WAL files should be archived first before cloning is started.
Note, that simply changing the version string in the `postgresql` manifest does
not work at present and leads to errors. Neither Patroni nor Postgres Operator
can do in place `pg_upgrade`. Still, it can be executed manually in the Postgres
container, which is tricky (i.e. systems need to be stopped, replicas have to be
synced) but of course faster than cloning.
## CRD Validation ## CRD Validation
[CustomResourceDefinitions](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#customresourcedefinitions) [CustomResourceDefinitions](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#customresourcedefinitions)
@ -95,8 +115,6 @@ is used by the operator to connect to the clusters after creation.
## Role-based access control for the operator ## Role-based access control for the operator
### Service account and cluster roles
The manifest [`operator-service-account-rbac.yaml`](../manifests/operator-service-account-rbac.yaml) The manifest [`operator-service-account-rbac.yaml`](../manifests/operator-service-account-rbac.yaml)
defines the service account, cluster roles and bindings needed for the operator defines the service account, cluster roles and bindings needed for the operator
to function under access control restrictions. To deploy the operator with this to function under access control restrictions. To deploy the operator with this
@ -110,6 +128,8 @@ kubectl create -f manifests/postgres-operator.yaml
kubectl create -f manifests/minimal-postgres-manifest.yaml kubectl create -f manifests/minimal-postgres-manifest.yaml
``` ```
### Service account and cluster roles
Note that the service account is named `zalando-postgres-operator`. You may have Note that the service account is named `zalando-postgres-operator`. You may have
to change the `service_account_name` in the operator ConfigMap and to change the `service_account_name` in the operator ConfigMap and
`serviceAccountName` in the `postgres-operator` deployment appropriately. This `serviceAccountName` in the `postgres-operator` deployment appropriately. This
@ -117,12 +137,6 @@ is done intentionally to avoid breaking those setups that already work with the
default `operator` account. In the future the operator should ideally be run default `operator` account. In the future the operator should ideally be run
under the `zalando-postgres-operator` service account. under the `zalando-postgres-operator` service account.
The service account defined in `operator-service-account-rbac.yaml` acquires
some privileges not used by the operator (i.e. we only need `list` and `watch`
on `configmaps` resources). This is also done intentionally to avoid breaking
things if someone decides to configure the same service account in the
operator's ConfigMap to run Postgres clusters.
### Give K8s users access to create/list `postgresqls` ### Give K8s users access to create/list `postgresqls`
By default `postgresql` custom resources can only be listed and changed by By default `postgresql` custom resources can only be listed and changed by
@ -158,7 +172,6 @@ metadata:
name: postgres-operator name: postgres-operator
data: data:
toleration: "key:postgres,operator:Exists,effect:NoSchedule" toleration: "key:postgres,operator:Exists,effect:NoSchedule"
...
``` ```
For an OperatorConfiguration resource the toleration should be defined like For an OperatorConfiguration resource the toleration should be defined like
@ -173,7 +186,6 @@ configuration:
kubernetes: kubernetes:
toleration: toleration:
postgres: "key:postgres,operator:Exists,effect:NoSchedule" postgres: "key:postgres,operator:Exists,effect:NoSchedule"
...
``` ```
Note that the K8s version 1.13 brings [taint-based eviction](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/#taint-based-evictions) Note that the K8s version 1.13 brings [taint-based eviction](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/#taint-based-evictions)
@ -251,7 +263,6 @@ metadata:
name: postgres-operator name: postgres-operator
data: data:
inherited_labels: application,environment inherited_labels: application,environment
...
``` ```
**OperatorConfiguration** **OperatorConfiguration**
@ -266,7 +277,6 @@ configuration:
inherited_labels: inherited_labels:
- application - application
- environment - environment
...
``` ```
**cluster manifest** **cluster manifest**
@ -280,7 +290,7 @@ metadata:
application: my-app application: my-app
environment: demo environment: demo
spec: spec:
... ...
``` ```
**network policy** **network policy**
@ -295,7 +305,6 @@ spec:
matchLabels: matchLabels:
application: my-app application: my-app
environment: demo environment: demo
...
``` ```
@ -318,7 +327,6 @@ metadata:
data: data:
# referencing config map with custom settings # referencing config map with custom settings
pod_environment_configmap: postgres-pod-config pod_environment_configmap: postgres-pod-config
...
``` ```
**OperatorConfiguration** **OperatorConfiguration**
@ -332,7 +340,6 @@ configuration:
kubernetes: kubernetes:
# referencing config map with custom settings # referencing config map with custom settings
pod_environment_configmap: postgres-pod-config pod_environment_configmap: postgres-pod-config
...
``` ```
**referenced ConfigMap `postgres-pod-config`** **referenced ConfigMap `postgres-pod-config`**
@ -413,12 +420,12 @@ external systems but defined for an individual Postgres cluster in its manifest.
A typical example is a role for connections from an application that uses the A typical example is a role for connections from an application that uses the
database. database.
* **Human users** originate from the Teams API that returns a list of the team * **Human users** originate from the [Teams API](user.md#teams-api-roles) that
members given a team id. The operator differentiates between (a) product teams returns a list of the team members given a team id. The operator differentiates
that own a particular Postgres cluster and are granted admin rights to maintain between (a) product teams that own a particular Postgres cluster and are granted
it, and (b) Postgres superuser teams that get the superuser access to all admin rights to maintain it, and (b) Postgres superuser teams that get the
Postgres databases running in a K8s cluster for the purposes of maintaining and superuser access to all Postgres databases running in a K8s cluster for the
troubleshooting. purposes of maintaining and troubleshooting.
## Understanding rolling update of Spilo pods ## Understanding rolling update of Spilo pods
@ -482,7 +489,7 @@ A secret can be pre-provisioned in different ways:
With the v1.2 release the Postgres Operator is shipped with a browser-based With the v1.2 release the Postgres Operator is shipped with a browser-based
configuration user interface (UI) that simplifies managing Postgres clusters configuration user interface (UI) that simplifies managing Postgres clusters
with the operator. The UI runs with Node.js and comes with it's own docker with the operator. The UI runs with Node.js and comes with it's own Docker
image. image.
Run NPM to continuously compile `tags/js` code. Basically, it creates an Run NPM to continuously compile `tags/js` code. Basically, it creates an
@ -494,14 +501,14 @@ Run NPM to continuously compile `tags/js` code. Basically, it creates an
To build the Docker image open a shell and change to the `ui` folder. Then run: To build the Docker image open a shell and change to the `ui` folder. Then run:
``` ```bash
docker build -t registry.opensource.zalan.do/acid/postgres-operator-ui:v1.2.0 . docker build -t registry.opensource.zalan.do/acid/postgres-operator-ui:v1.2.0 .
``` ```
Apply all manifests for the `ui/manifests` folder to deploy the Postgres Apply all manifests for the `ui/manifests` folder to deploy the Postgres
Operator UI on K8s. For local tests you don't need the Ingress resource. Operator UI on K8s. For local tests you don't need the Ingress resource.
``` ```bash
kubectl apply -f ui/manifests kubectl apply -f ui/manifests
``` ```
@ -511,6 +518,6 @@ to the K8s and Postgres Operator REST API. You can use the provided
`run_local.sh` script for this. Make sure it uses the correct URL to your K8s `run_local.sh` script for this. Make sure it uses the correct URL to your K8s
API server, e.g. for minikube it would be `https://192.168.99.100:8443`. API server, e.g. for minikube it would be `https://192.168.99.100:8443`.
``` ```bash
./run_local.sh ./run_local.sh
``` ```

View File

@ -40,7 +40,7 @@ This would take a while to complete. You have to redo `make deps` every time
your dependencies list changes, i.e. after adding a new library dependency. your dependencies list changes, i.e. after adding a new library dependency.
Build the operator with the `make docker` command. You may define the TAG 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 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 the image name. By default, the tag is computed with
`git describe --tags --always --dirty` and the image is `git describe --tags --always --dirty` and the image is
`registry.opensource.zalan.do/acid/postgres-operator` `registry.opensource.zalan.do/acid/postgres-operator`
@ -60,10 +60,10 @@ The binary will be placed into the build directory.
## Deploying self build image ## Deploying self build image
The fastest way to run and test your docker image locally is to reuse the docker The fastest way to run and test your Docker image locally is to reuse the Docker
from [minikube](https://github.com/kubernetes/minikube/releases) or use the environment from [minikube](https://github.com/kubernetes/minikube/releases)
`load docker-image` from [kind](https://kind.sigs.k8s.io/). The following steps or use the `load docker-image` from [kind](https://kind.sigs.k8s.io/). The
will get you the docker image built and deployed. following steps will get you the Docker image built and deployed.
```bash ```bash
# minikube # minikube
@ -163,7 +163,7 @@ The operator also supports pprof endpoints listed at the
* /debug/pprof/trace * /debug/pprof/trace
It's possible to attach a debugger to troubleshoot postgres-operator inside a It's possible to attach a debugger to troubleshoot postgres-operator inside a
docker container. It's possible with [gdb](https://www.gnu.org/software/gdb/) Docker container. It's possible with [gdb](https://www.gnu.org/software/gdb/)
and [delve](https://github.com/derekparker/delve). Since the latter one is a and [delve](https://github.com/derekparker/delve). Since the latter one is a
specialized debugger for Go, we will use it as an example. To use it you need: specialized debugger for Go, we will use it as an example. To use it you need:

View File

@ -13,7 +13,7 @@ manages PostgreSQL clusters on Kubernetes (K8s):
2. The operator also watches updates to [its own configuration](../manifests/configmap.yaml) 2. The operator also watches updates to [its own configuration](../manifests/configmap.yaml)
and alters running Postgres clusters if necessary. For instance, if the and alters running Postgres clusters if necessary. For instance, if the
docker image in a pod is changed, the operator carries out the rolling Docker image in a pod is changed, the operator carries out the rolling
update, which means it re-spawns pods of each managed StatefulSet one-by-one update, which means it re-spawns pods of each managed StatefulSet one-by-one
with the new Docker image. with the new Docker image.

View File

@ -157,9 +157,12 @@ export PGPORT=$(echo $HOST_PORT | cut -d: -f 2)
``` ```
Retrieve the password from the K8s Secret that is created in your cluster. Retrieve the password from the K8s Secret that is created in your cluster.
Non-encrypted connections are rejected by default, so set the SSL mode to
require:
```bash ```bash
export PGPASSWORD=$(kubectl get secret postgres.acid-minimal-cluster.credentials -o 'jsonpath={.data.password}' | base64 -d) export PGPASSWORD=$(kubectl get secret postgres.acid-minimal-cluster.credentials -o 'jsonpath={.data.password}' | base64 -d)
export PGSSLMODE=require
psql -U postgres psql -U postgres
``` ```

View File

@ -62,7 +62,7 @@ These parameters are grouped directly under the `spec` key in the manifest.
field. field.
* **dockerImage** * **dockerImage**
custom docker image that overrides the **docker_image** operator parameter. custom Docker image that overrides the **docker_image** operator parameter.
It should be a [Spilo](https://github.com/zalando/spilo) image. Optional. It should be a [Spilo](https://github.com/zalando/spilo) image. Optional.
* **spiloFSGroup** * **spiloFSGroup**
@ -124,7 +124,7 @@ These parameters are grouped directly under the `spec` key in the manifest.
* **enableShmVolume** * **enableShmVolume**
Start a database pod without limitations on shm memory. By default docker Start a database pod without limitations on shm memory. By default Docker
limit `/dev/shm` to `64M` (see e.g. the [docker limit `/dev/shm` to `64M` (see e.g. the [docker
issue](https://github.com/docker-library/postgres/issues/416), which could be issue](https://github.com/docker-library/postgres/issues/416), which could be
not enough if PostgreSQL uses parallel workers heavily. If this option is not enough if PostgreSQL uses parallel workers heavily. If this option is
@ -185,19 +185,19 @@ explanation of `ttl` and `loop_wait` parameters.
* **ttl** * **ttl**
Patroni `ttl` parameter value, optional. The default is set by the Spilo Patroni `ttl` parameter value, optional. The default is set by the Spilo
docker image. Optional. Docker image. Optional.
* **loop_wait** * **loop_wait**
Patroni `loop_wait` parameter value, optional. The default is set by the Patroni `loop_wait` parameter value, optional. The default is set by the
Spilo docker image. Optional. Spilo Docker image. Optional.
* **retry_timeout** * **retry_timeout**
Patroni `retry_timeout` parameter value, optional. The default is set by the Patroni `retry_timeout` parameter value, optional. The default is set by the
Spilo docker image. Optional. Spilo Docker image. Optional.
* **maximum_lag_on_failover** * **maximum_lag_on_failover**
Patroni `maximum_lag_on_failover` parameter value, optional. The default is Patroni `maximum_lag_on_failover` parameter value, optional. The default is
set by the Spilo docker image. Optional. set by the Spilo Docker image. Optional.
* **slots** * **slots**
permanent replication slots that Patroni preserves after failover by permanent replication slots that Patroni preserves after failover by
@ -320,7 +320,7 @@ defined in the sidecar dictionary:
name of the sidecar. Required. name of the sidecar. Required.
* **image** * **image**
docker image of the sidecar. Required. Docker image of the sidecar. Required.
* **env** * **env**
a dictionary of environment variables. Use usual Kubernetes definition a dictionary of environment variables. Use usual Kubernetes definition

View File

@ -81,15 +81,15 @@ Those are top-level keys, containing both leaf keys and groups.
Kubernetes-native DCS). Kubernetes-native DCS).
* **docker_image** * **docker_image**
Spilo docker image for Postgres instances. For production, don't rely on the Spilo Docker image for Postgres instances. For production, don't rely on the
default image, as it might be not the most up-to-date one. Instead, build default image, as it might be not the most up-to-date one. Instead, build
your own Spilo image from the [github your own Spilo image from the [github
repository](https://github.com/zalando/spilo). repository](https://github.com/zalando/spilo).
* **sidecar_docker_images** * **sidecar_docker_images**
a map of sidecar names to docker images for the containers to run alongside a map of sidecar names to Docker images to run with Spilo. In case of the name
Spilo. In case of the name conflict with the definition in the cluster conflict with the definition in the cluster manifest the cluster-specific one
manifest the cluster-specific one is preferred. is preferred.
* **enable_shm_volume** * **enable_shm_volume**
Instruct operator to start any new database pod without limitations on shm Instruct operator to start any new database pod without limitations on shm
@ -196,6 +196,14 @@ configuration they are grouped under the `kubernetes` key.
[admin docs](../administrator.md#pod-disruption-budget) for more information. [admin docs](../administrator.md#pod-disruption-budget) for more information.
Default is true. Default is true.
* **enable_init_containers**
global option to allow for creating init containers to run actions before
Spilo is started. Default is true.
* **enable_sidecars**
global option to allow for creating sidecar containers to run alongside Spilo
on the same pod. Default is true.
* **secret_name_template** * **secret_name_template**
a template for the name of the database user secrets generated by the a template for the name of the database user secrets generated by the
operator. `{username}` is replaced with name of the secret, `{cluster}` with operator. `{username}` is replaced with name of the secret, `{cluster}` with

View File

@ -13,7 +13,7 @@ kind: postgresql
metadata: metadata:
name: acid-minimal-cluster name: acid-minimal-cluster
spec: spec:
teamId: "ACID" teamId: "acid"
volume: volume:
size: 1Gi size: 1Gi
numberOfInstances: 2 numberOfInstances: 2
@ -30,7 +30,7 @@ spec:
databases: databases:
foo: zalando foo: zalando
postgresql: postgresql:
version: "10" version: "11"
``` ```
Once you cloned the Postgres Operator [repository](https://github.com/zalando/postgres-operator) Once you cloned the Postgres Operator [repository](https://github.com/zalando/postgres-operator)
@ -40,6 +40,17 @@ you can find this example also in the manifests folder:
kubectl create -f manifests/minimal-postgres-manifest.yaml kubectl create -f manifests/minimal-postgres-manifest.yaml
``` ```
Make sure, the `spec` section of the manifest contains at least a `teamId`, the
`numberOfInstances` and the `postgresql` object with the `version` specified.
The minimum volume size to run the `postgresql` resource on Elastic Block
Storage (EBS) is `1Gi`.
Note, that the name of the cluster must start with the `teamId` and `-`. At
Zalando we use team IDs (nicknames) to lower the chance of duplicate cluster
names and colliding entities. The team ID would also be used to query an API to
get all members of a team and create [database roles](#teams-api-roles) for
them.
## Watch pods being created ## Watch pods being created
```bash ```bash
@ -62,10 +73,12 @@ kubectl port-forward $PGMASTER 6432:5432
Open another CLI and connect to the database. Use the generated secret of the Open another CLI and connect to the database. Use the generated secret of the
`postgres` robot user to connect to our `acid-minimal-cluster` master running `postgres` robot user to connect to our `acid-minimal-cluster` master running
in Minikube: in Minikube. As non-encrypted connections are rejected by default set the SSL
mode to require:
```bash ```bash
export PGPASSWORD=$(kubectl get secret postgres.acid-minimal-cluster.credentials -o 'jsonpath={.data.password}' | base64 -d) export PGPASSWORD=$(kubectl get secret postgres.acid-minimal-cluster.credentials -o 'jsonpath={.data.password}' | base64 -d)
export PGSSLMODE=require
psql -U postgres -p 6432 psql -U postgres -p 6432
``` ```
@ -77,8 +90,7 @@ cluster. It covers three use-cases:
* `manifest roles`: create application roles specific to the cluster described * `manifest roles`: create application roles specific to the cluster described
in the manifest. in the manifest.
* `infrastructure roles`: create application roles that should be automatically * `infrastructure roles`: create application roles that should be automatically
created on every created on every cluster managed by the operator.
cluster managed by the operator.
* `teams API roles`: automatically create users for every member of the team * `teams API roles`: automatically create users for every member of the team
owning the database cluster. owning the database cluster.
@ -128,9 +140,9 @@ The infrastructure roles secret is specified by the `infrastructure_roles_secret
parameter. The role definition looks like this (values are base64 encoded): parameter. The role definition looks like this (values are base64 encoded):
```yaml ```yaml
user1: ZGJ1c2Vy user1: ZGJ1c2Vy
password1: c2VjcmV0 password1: c2VjcmV0
inrole1: b3BlcmF0b3I= inrole1: b3BlcmF0b3I=
``` ```
The block above describes the infrastructure role 'dbuser' with password The block above describes the infrastructure role 'dbuser' with password
@ -151,19 +163,19 @@ secret and a ConfigMap. The ConfigMap must have the same name as the secret.
The secret should contain an entry with 'rolename:rolepassword' for each role. The secret should contain an entry with 'rolename:rolepassword' for each role.
```yaml ```yaml
dbuser: c2VjcmV0 dbuser: c2VjcmV0
``` ```
And the role description for that user should be specified in the ConfigMap. And the role description for that user should be specified in the ConfigMap.
```yaml ```yaml
data: data:
dbuser: | dbuser: |
inrole: [operator, admin] # following roles will be assigned to the new user inrole: [operator, admin] # following roles will be assigned to the new user
user_flags: user_flags:
- createdb - createdb
db_parameters: # db parameters, applied for this particular user db_parameters: # db parameters, applied for this particular user
log_statement: all log_statement: all
``` ```
One can allow membership in multiple roles via the `inrole` array parameter, One can allow membership in multiple roles via the `inrole` array parameter,
@ -182,6 +194,50 @@ See [infrastructure roles secret](../manifests/infrastructure-roles.yaml)
and [infrastructure roles configmap](../manifests/infrastructure-roles-configmap.yaml) and [infrastructure roles configmap](../manifests/infrastructure-roles-configmap.yaml)
for the examples. for the examples.
### Teams API roles
These roles are meant for database activity of human users. It's possible to
configure the operator to automatically create database roles for lets say all
employees of one team. They are not listed in the manifest and there are no K8s
secrets created for them. Instead they would use an OAuth2 token to connect. To
get all members of the team the operator queries a defined API endpoint that
returns usernames. A minimal Teams API should work like this:
```
/.../<teamname> -> ["name","anothername"]
```
A ["fake" Teams API](../manifests/fake-teams-api.yaml) deployment is provided
in the manifests folder to set up a basic API around whatever services is used
for user management. The Teams API's URL is set in the operator's
[configuration](reference/operator_parameters.md#automatic-creation-of-human-users-in-the-database)
and `enable_teams_api` must be set to `true`. There are more settings available
to choose superusers, group roles, [PAM configuration](https://github.com/CyberDem0n/pam-oauth2)
etc. An OAuth2 token can be passed to the Teams API via a secret. The name for
this secret is configurable with the `oauth_token_secret_name` parameter.
## Resource definition
The compute resources to be used for the Postgres containers in the pods can be
specified in the postgresql cluster manifest.
```yaml
spec:
resources:
requests:
cpu: 10m
memory: 100Mi
limits:
cpu: 300m
memory: 300Mi
```
The minimum limit to properly run the `postgresql` resource is `256m` for `cpu`
and `256Mi` for `memory`. If a lower value is set in the manifest the operator
will cancel ADD or UPDATE events on this resource with an error. If no
resources are defined in the manifest the operator will obtain the configured
[default requests](reference/operator_parameters.md#kubernetes-resource-requests).
## Use taints and tolerations for dedicated PostgreSQL nodes ## Use taints and tolerations for dedicated PostgreSQL nodes
To ensure Postgres pods are running on nodes without any other application pods, To ensure Postgres pods are running on nodes without any other application pods,
@ -189,12 +245,7 @@ you can use [taints and tolerations](https://kubernetes.io/docs/concepts/configu
and configure the required toleration in the manifest. and configure the required toleration in the manifest.
```yaml ```yaml
apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
name: acid-minimal-cluster
spec: spec:
teamId: "ACID"
tolerations: tolerations:
- key: postgres - key: postgres
operator: Exists operator: Exists
@ -212,11 +263,6 @@ section in the spec. There are two options here:
### Clone directly ### Clone directly
```yaml ```yaml
apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
name: acid-test-cluster
spec: spec:
clone: clone:
cluster: "acid-batman" cluster: "acid-batman"
@ -232,11 +278,6 @@ means that you can clone only from clusters within the same namespace.
### Clone from S3 ### Clone from S3
```yaml ```yaml
apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
name: acid-test-cluster
spec: spec:
clone: clone:
uid: "efd12e58-5786-11e8-b5a7-06148230260c" uid: "efd12e58-5786-11e8-b5a7-06148230260c"
@ -265,10 +306,6 @@ For non AWS S3 following settings can be set to support cloning from other S3
implementations: implementations:
```yaml ```yaml
apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
name: acid-test-cluster
spec: spec:
clone: clone:
uid: "efd12e58-5786-11e8-b5a7-06148230260c" uid: "efd12e58-5786-11e8-b5a7-06148230260c"
@ -305,7 +342,7 @@ Things to note:
- There is no way to transform a non-standby cluster to a standby cluster - There is no way to transform a non-standby cluster to a standby cluster
through the operator. Adding the standby section to the manifest of a running through the operator. Adding the standby section to the manifest of a running
Postgres cluster will have no effect. However, it can be done through Patroni Postgres cluster will have no effect. However, it can be done through Patroni
by adding the [standby_cluster] (https://github.com/zalando/patroni/blob/bd2c54581abb42a7d3a3da551edf0b8732eefd27/docs/replica_bootstrap.rst#standby-cluster) by adding the [standby_cluster](https://github.com/zalando/patroni/blob/bd2c54581abb42a7d3a3da551edf0b8732eefd27/docs/replica_bootstrap.rst#standby-cluster)
section using `patronictl edit-config`. Note that the transformed standby section using `patronictl edit-config`. Note that the transformed standby
cluster will not be doing any streaming. It will be in standby mode and allow cluster will not be doing any streaming. It will be in standby mode and allow
read-only transactions only. read-only transactions only.
@ -317,13 +354,7 @@ used for log aggregation, monitoring, backups or other tasks. A sidecar can be
specified like this: specified like this:
```yaml ```yaml
apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
name: acid-minimal-cluster
spec: spec:
...
sidecars: sidecars:
- name: "container-name" - name: "container-name"
image: "company/image:tag" image: "company/image:tag"
@ -350,6 +381,10 @@ variables are always passed to sidecars:
The PostgreSQL volume is shared with sidecars and is mounted at The PostgreSQL volume is shared with sidecars and is mounted at
`/home/postgres/pgdata`. `/home/postgres/pgdata`.
**Note**: The operator will not create a cluster if sidecar containers are
specified but globally disabled in the configuration. The `enable_sidecars`
option must be set to `true`.
## InitContainers Support ## InitContainers Support
Each cluster can specify arbitrary init containers to run. These containers can Each cluster can specify arbitrary init containers to run. These containers can
@ -357,13 +392,7 @@ be used to run custom actions before any normal and sidecar containers start.
An init container can be specified like this: An init container can be specified like this:
```yaml ```yaml
apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
name: acid-minimal-cluster
spec: spec:
...
initContainers: initContainers:
- name: "container-name" - name: "container-name"
image: "company/image:tag" image: "company/image:tag"
@ -374,18 +403,17 @@ spec:
`initContainers` accepts full `v1.Container` definition. `initContainers` accepts full `v1.Container` definition.
**Note**: The operator will not create a cluster if `initContainers` are
specified but globally disabled in the configuration. The
`enable_init_containers` option must be set to `true`.
## Increase volume size ## Increase volume size
PostgreSQL operator supports statefulset volume resize if you're using the Postgres operator supports statefulset volume resize if you're using the
operator on top of AWS. For that you need to change the size field of the operator on top of AWS. For that you need to change the size field of the
volume description in the cluster manifest and apply the change: volume description in the cluster manifest and apply the change:
``` ```yaml
apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
name: acid-test-cluster
spec: spec:
volume: volume:
size: 5Gi # new volume size size: 5Gi # new volume size
@ -414,7 +442,8 @@ size of volumes that correspond to the previously running pods is not changed.
You can enable logical backups from the cluster manifest by adding the following You can enable logical backups from the cluster manifest by adding the following
parameter in the spec section: parameter in the spec section:
``` ```yaml
spec:
enableLogicalBackup: true enableLogicalBackup: true
``` ```

View File

@ -179,20 +179,14 @@ class EndToEndTestCase(unittest.TestCase):
# update the cluster-wide image of the logical backup pod # update the cluster-wide image of the logical backup pod
image = "test-image-name" image = "test-image-name"
config_patch = { patch_logical_backup_image = {
"configuration": { "configuration": {
"logical_backup": { "logical_backup": {
"logical_backup_docker_image": image, "logical_backup_docker_image": image,
} }
} }
} }
k8s.api.custom_objects_api.patch_namespaced_custom_object( k8s.update_config(patch_logical_backup_image)
"acid.zalan.do", "v1", "default", "operatorconfigurations", "postgresql-operator-default-configuration", config_patch)
operator_pod = k8s.api.core_v1.list_namespaced_pod(
'default', label_selector="name=postgres-operator").items[0].metadata.name
k8s.api.core_v1.delete_namespaced_pod(operator_pod, "default") # restart reloads the conf
k8s.wait_for_operator_pod_start()
jobs = k8s.get_logical_backup_job().items jobs = k8s.get_logical_backup_job().items
actual_image = jobs[0].spec.job_template.spec.template.spec.containers[0].image actual_image = jobs[0].spec.job_template.spec.template.spec.containers[0].image
@ -319,12 +313,21 @@ class K8s:
def wait_for_logical_backup_job_creation(self): def wait_for_logical_backup_job_creation(self):
self.wait_for_logical_backup_job(expected_num_of_jobs=1) self.wait_for_logical_backup_job(expected_num_of_jobs=1)
def apply_kustomization(self, path): def update_config(self, patch):
subprocess.run(["kubectl", "apply", "-k", path]) k8s.api.custom_objects_api.patch_namespaced_custom_object(
"acid.zalan.do", "v1", "default", "operatorconfigurations", "postgresql-operator-default-configuration", patch)
operator_pod = self.api.core_v1.list_namespaced_pod(
'default', label_selector="name=postgres-operator").items[0].metadata.name
self.api.core_v1.delete_namespaced_pod(operator_pod, "default") # restart reloads the conf
self.wait_for_operator_pod_start()
def create_with_kubectl(self, path): def create_with_kubectl(self, path):
subprocess.run(["kubectl", "create", "-f", path]) subprocess.run(["kubectl", "create", "-f", path])
def apply_kustomization(self, path):
subprocess.run(["kubectl", "apply", "-k", path])
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

20
go.mod
View File

@ -3,23 +3,23 @@ module github.com/zalando/postgres-operator
go 1.12 go 1.12
require ( require (
github.com/aws/aws-sdk-go v1.25.1 github.com/aws/aws-sdk-go v1.25.44
github.com/emicklei/go-restful v2.9.6+incompatible // indirect github.com/emicklei/go-restful v2.9.6+incompatible // indirect
github.com/evanphx/json-patch v4.5.0+incompatible // indirect github.com/evanphx/json-patch v4.5.0+incompatible // indirect
github.com/googleapis/gnostic v0.3.0 // indirect github.com/googleapis/gnostic v0.3.0 // indirect
github.com/imdario/mergo v0.3.7 // indirect github.com/imdario/mergo v0.3.8 // indirect
github.com/lib/pq v1.2.0 github.com/lib/pq v1.2.0
github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d
github.com/sirupsen/logrus v1.4.2 github.com/sirupsen/logrus v1.4.2
golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c // indirect golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 // indirect golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect
golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f // indirect golang.org/x/tools v0.0.0-20191209225234-22774f7dae43 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.2.5 gopkg.in/yaml.v2 v2.2.4
k8s.io/api v0.0.0-20191121015604-11707872ac1c k8s.io/api v0.0.0-20191121015604-11707872ac1c
k8s.io/apiextensions-apiserver v0.0.0-20191121021419-88daf26ec3b8 k8s.io/apiextensions-apiserver v0.0.0-20191204090421-cd61debedab5
k8s.io/apimachinery v0.0.0-20191121015412-41065c7a8c2a k8s.io/apimachinery v0.0.0-20191203211716-adc6f4cd9e7d
k8s.io/client-go v11.0.0+incompatible k8s.io/client-go v0.0.0-20191204082520-bc9b51d240b2
k8s.io/code-generator v0.0.0-20191121015212-c4c8f8345c7e k8s.io/code-generator v0.0.0-20191121015212-c4c8f8345c7e
) )

View File

@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -o errexit set -o errexit
set -o nounset set -o nounset

View File

@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -o errexit set -o errexit
set -o nounset set -o nounset

View File

@ -10,7 +10,7 @@ spec:
- name: date - name: date
image: busybox image: busybox
command: [ "/bin/date" ] command: [ "/bin/date" ]
teamId: "ACID" teamId: "acid"
volume: volume:
size: 1Gi size: 1Gi
# storageClass: my-sc # storageClass: my-sc

View File

@ -23,11 +23,13 @@ data:
# enable_admin_role_for_users: "true" # enable_admin_role_for_users: "true"
# enable_crd_validation: "true" # enable_crd_validation: "true"
# enable_database_access: "true" # enable_database_access: "true"
# enable_init_containers: "true"
enable_master_load_balancer: "false" enable_master_load_balancer: "false"
# enable_pod_antiaffinity: "false" # enable_pod_antiaffinity: "false"
# enable_pod_disruption_budget: "true" # enable_pod_disruption_budget: "true"
enable_replica_load_balancer: "false" enable_replica_load_balancer: "false"
# enable_shm_volume: "true" # enable_shm_volume: "true"
# enable_sidecars: "true"
# enable_team_superuser: "false" # enable_team_superuser: "false"
enable_teams_api: "false" enable_teams_api: "false"
# etcd_host: "" # etcd_host: ""

View File

@ -4,7 +4,7 @@ metadata:
name: acid-minimal-cluster name: acid-minimal-cluster
namespace: default namespace: default
spec: spec:
teamId: "ACID" teamId: "acid"
volume: volume:
size: 1Gi size: 1Gi
numberOfInstances: 2 numberOfInstances: 2

View File

@ -83,10 +83,14 @@ spec:
type: object type: object
additionalProperties: additionalProperties:
type: string type: string
enable_init_containers:
type: boolean
enable_pod_antiaffinity: enable_pod_antiaffinity:
type: boolean type: boolean
enable_pod_disruption_budget: enable_pod_disruption_budget:
type: boolean type: boolean
enable_sidecars:
type: boolean
infrastructure_roles_secret_name: infrastructure_roles_secret_name:
type: string type: string
inherited_labels: inherited_labels:
@ -274,3 +278,7 @@ spec:
pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$'
scalyr_server_url: scalyr_server_url:
type: string type: string
status:
type: object
additionalProperties:
type: string

View File

@ -26,14 +26,16 @@ configuration:
# custom_pod_annotations: # custom_pod_annotations:
# keya: valuea # keya: valuea
# keyb: valueb # keyb: valueb
enable_init_containers: true
enable_pod_antiaffinity: false enable_pod_antiaffinity: false
enable_pod_disruption_budget: true enable_pod_disruption_budget: true
# infrastructure_roles_secret_name: postgresql-infrastructure-roles enable_sidecars: true
# infrastructure_roles_secret_name: "postgresql-infrastructure-roles"
# inherited_labels: # inherited_labels:
# - application # - application
# - environment # - environment
# node_readiness_label: # node_readiness_label:
# status: ready # status: ready
oauth_token_secret_name: postgresql-operator oauth_token_secret_name: postgresql-operator
pdb_name_format: "postgres-{cluster}-pdb" pdb_name_format: "postgres-{cluster}-pdb"
pod_antiaffinity_topology_key: "kubernetes.io/hostname" pod_antiaffinity_topology_key: "kubernetes.io/hostname"

View File

@ -186,7 +186,7 @@ spec:
# only the format of the given number. # only the format of the given number.
# #
# https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu # https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu
pattern: '^(\d+m|\d+\.\d{1,3})$' pattern: '^(\d+m|\d+(\.\d{1,3})?)$'
# Note: the value specified here must not be zero or be lower # Note: the value specified here must not be zero or be lower
# than the corresponding request. # than the corresponding request.
memory: memory:
@ -217,7 +217,7 @@ spec:
# only the format of the given number. # only the format of the given number.
# #
# https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu # https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu
pattern: '^(\d+m|\d+\.\d{1,3})$' pattern: '^(\d+m|\d+(\.\d{1,3})?)$'
# Note: the value specified here must not be zero or be higher # Note: the value specified here must not be zero or be higher
# than the corresponding limit. # than the corresponding limit.
memory: memory:
@ -325,3 +325,7 @@ spec:
type: string type: string
subPath: subPath:
type: string type: string
status:
type: object
additionalProperties:
type: string

View File

@ -4,7 +4,7 @@ metadata:
name: acid-standby-cluster name: acid-standby-cluster
namespace: default namespace: default
spec: spec:
teamId: "ACID" teamId: "acid"
volume: volume:
size: 1Gi size: 1Gi
numberOfInstances: 1 numberOfInstances: 1

View File

@ -356,7 +356,7 @@ var PostgresCRDResourceValidation = apiextv1beta1.CustomResourceValidation{
"cpu": { "cpu": {
Type: "string", Type: "string",
Description: "Decimal natural followed by m, or decimal natural followed by dot followed by up to three decimal digits (precision used by Kubernetes). Must be greater than 0", Description: "Decimal natural followed by m, or decimal natural followed by dot followed by up to three decimal digits (precision used by Kubernetes). Must be greater than 0",
Pattern: "^(\\d+m|\\d+\\.\\d{1,3})$", Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$",
}, },
"memory": { "memory": {
Type: "string", Type: "string",
@ -372,7 +372,7 @@ var PostgresCRDResourceValidation = apiextv1beta1.CustomResourceValidation{
"cpu": { "cpu": {
Type: "string", Type: "string",
Description: "Decimal natural followed by m, or decimal natural followed by dot followed by up to three decimal digits (precision used by Kubernetes). Must be greater than 0", Description: "Decimal natural followed by m, or decimal natural followed by dot followed by up to three decimal digits (precision used by Kubernetes). Must be greater than 0",
Pattern: "^(\\d+m|\\d+\\.\\d{1,3})$", Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$",
}, },
"memory": { "memory": {
Type: "string", Type: "string",
@ -578,6 +578,14 @@ var PostgresCRDResourceValidation = apiextv1beta1.CustomResourceValidation{
}, },
}, },
}, },
"status": {
Type: "object",
AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{
Schema: &apiextv1beta1.JSONSchemaProps{
Type: "string",
},
},
},
}, },
}, },
} }
@ -686,12 +694,18 @@ var OperatorConfigCRDResourceValidation = apiextv1beta1.CustomResourceValidation
}, },
}, },
}, },
"enable_init_containers": {
Type: "boolean",
},
"enable_pod_antiaffinity": { "enable_pod_antiaffinity": {
Type: "boolean", Type: "boolean",
}, },
"enable_pod_disruption_budget": { "enable_pod_disruption_budget": {
Type: "boolean", Type: "boolean",
}, },
"enable_sidecars": {
Type: "boolean",
},
"infrastructure_roles_secret_name": { "infrastructure_roles_secret_name": {
Type: "string", Type: "string",
}, },
@ -994,6 +1008,14 @@ var OperatorConfigCRDResourceValidation = apiextv1beta1.CustomResourceValidation
}, },
}, },
}, },
"status": {
Type: "object",
AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{
Schema: &apiextv1beta1.JSONSchemaProps{
Type: "string",
},
},
},
}, },
}, },
} }

View File

@ -50,6 +50,8 @@ type KubernetesMetaConfiguration struct {
WatchedNamespace string `json:"watched_namespace,omitempty"` WatchedNamespace string `json:"watched_namespace,omitempty"`
PDBNameFormat config.StringTemplate `json:"pdb_name_format,omitempty"` PDBNameFormat config.StringTemplate `json:"pdb_name_format,omitempty"`
EnablePodDisruptionBudget *bool `json:"enable_pod_disruption_budget,omitempty"` EnablePodDisruptionBudget *bool `json:"enable_pod_disruption_budget,omitempty"`
EnableInitContainers *bool `json:"enable_init_containers,omitempty"`
EnableSidecars *bool `json:"enable_sidecars,omitempty"`
SecretNameTemplate config.StringTemplate `json:"secret_name_template,omitempty"` SecretNameTemplate config.StringTemplate `json:"secret_name_template,omitempty"`
ClusterDomain string `json:"cluster_domain"` ClusterDomain string `json:"cluster_domain"`
OAuthTokenSecretName spec.NamespacedName `json:"oauth_token_secret_name,omitempty"` OAuthTokenSecretName spec.NamespacedName `json:"oauth_token_secret_name,omitempty"`

View File

@ -180,7 +180,7 @@ var unmarshalCluster = []struct {
"name": "acid-testcluster1" "name": "acid-testcluster1"
}, },
"spec": { "spec": {
"teamId": "ACID", "teamId": "acid",
"pod_priority_class_name": "spilo-pod-priority", "pod_priority_class_name": "spilo-pod-priority",
"volume": { "volume": {
"size": "5Gi", "size": "5Gi",
@ -290,7 +290,7 @@ var unmarshalCluster = []struct {
ResourceLimits: ResourceDescription{CPU: "300m", Memory: "3000Mi"}, ResourceLimits: ResourceDescription{CPU: "300m", Memory: "3000Mi"},
}, },
TeamID: "ACID", TeamID: "acid",
AllowedSourceRanges: []string{"127.0.0.1/32"}, AllowedSourceRanges: []string{"127.0.0.1/32"},
NumberOfInstances: 2, NumberOfInstances: 2,
Users: map[string]UserFlags{"zalando": {"superuser", "createdb"}}, Users: map[string]UserFlags{"zalando": {"superuser", "createdb"}},
@ -319,7 +319,7 @@ var unmarshalCluster = []struct {
}, },
Error: "", Error: "",
}, },
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"9.6","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":""}}`), marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"9.6","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},
// example with teamId set in input // example with teamId set in input
{ {

View File

@ -81,6 +81,16 @@ func (in *KubernetesMetaConfiguration) DeepCopyInto(out *KubernetesMetaConfigura
*out = new(bool) *out = new(bool)
**out = **in **out = **in
} }
if in.EnableInitContainers != nil {
in, out := &in.EnableInitContainers, &out.EnableInitContainers
*out = new(bool)
**out = **in
}
if in.EnableSidecars != nil {
in, out := &in.EnableSidecars, &out.EnableSidecars
*out = new(bool)
**out = **in
}
out.OAuthTokenSecretName = in.OAuthTokenSecretName out.OAuthTokenSecretName = in.OAuthTokenSecretName
out.InfrastructureRolesSecretName = in.InfrastructureRolesSecretName out.InfrastructureRolesSecretName = in.InfrastructureRolesSecretName
if in.ClusterLabels != nil { if in.ClusterLabels != nil {

View File

@ -227,6 +227,10 @@ func (c *Cluster) Create() error {
c.setStatus(acidv1.ClusterStatusCreating) c.setStatus(acidv1.ClusterStatusCreating)
if err = c.validateResources(&c.Spec); err != nil {
return fmt.Errorf("insufficient resource limits specified: %v", err)
}
for _, role := range []PostgresRole{Master, Replica} { for _, role := range []PostgresRole{Master, Replica} {
if c.Endpoints[role] != nil { if c.Endpoints[role] != nil {
@ -491,6 +495,44 @@ func compareResourcesAssumeFirstNotNil(a *v1.ResourceRequirements, b *v1.Resourc
} }
func (c *Cluster) validateResources(spec *acidv1.PostgresSpec) error {
// setting limits too low can cause unnecessary evictions / OOM kills
const (
cpuMinLimit = "256m"
memoryMinLimit = "256Mi"
)
var (
isSmaller bool
err error
)
cpuLimit := spec.Resources.ResourceLimits.CPU
if cpuLimit != "" {
isSmaller, err = util.IsSmallerQuantity(cpuLimit, cpuMinLimit)
if err != nil {
return fmt.Errorf("error validating CPU limit: %v", err)
}
if isSmaller {
return fmt.Errorf("defined CPU limit %s is below required minimum %s to properly run postgresql resource", cpuLimit, cpuMinLimit)
}
}
memoryLimit := spec.Resources.ResourceLimits.Memory
if memoryLimit != "" {
isSmaller, err = util.IsSmallerQuantity(memoryLimit, memoryMinLimit)
if err != nil {
return fmt.Errorf("error validating memory limit: %v", err)
}
if isSmaller {
return fmt.Errorf("defined memory limit %s is below required minimum %s to properly run postgresql resource", memoryLimit, memoryMinLimit)
}
}
return nil
}
// Update changes Kubernetes objects according to the new specification. Unlike the sync case, the missing object // Update changes Kubernetes objects according to the new specification. Unlike the sync case, the missing object
// (i.e. service) is treated as an error // (i.e. service) is treated as an error
// logical backup cron jobs are an exception: a user-initiated Update can enable a logical backup job // logical backup cron jobs are an exception: a user-initiated Update can enable a logical backup job
@ -501,6 +543,7 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
oldStatus := c.Status
c.setStatus(acidv1.ClusterStatusUpdating) c.setStatus(acidv1.ClusterStatusUpdating)
c.setSpec(newSpec) c.setSpec(newSpec)
@ -512,6 +555,22 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
} }
}() }()
if err := c.validateResources(&newSpec.Spec); err != nil {
err = fmt.Errorf("insufficient resource limits specified: %v", err)
// cancel update only when (already too low) pod resources were edited
// if cluster was successfully running before the update, continue but log a warning
isCPULimitSmaller, err2 := util.IsSmallerQuantity(newSpec.Spec.Resources.ResourceLimits.CPU, oldSpec.Spec.Resources.ResourceLimits.CPU)
isMemoryLimitSmaller, err3 := util.IsSmallerQuantity(newSpec.Spec.Resources.ResourceLimits.Memory, oldSpec.Spec.Resources.ResourceLimits.Memory)
if oldStatus.Running() && !isCPULimitSmaller && !isMemoryLimitSmaller && err2 == nil && err3 == nil {
c.logger.Warning(err)
} else {
updateFailed = true
return err
}
}
if oldSpec.Spec.PgVersion != newSpec.Spec.PgVersion { // PG versions comparison if oldSpec.Spec.PgVersion != newSpec.Spec.PgVersion { // PG versions comparison
c.logger.Warningf("postgresql version change(%q -> %q) has no effect", oldSpec.Spec.PgVersion, newSpec.Spec.PgVersion) c.logger.Warningf("postgresql version change(%q -> %q) has no effect", oldSpec.Spec.PgVersion, newSpec.Spec.PgVersion)
//we need that hack to generate statefulset with the old version //we need that hack to generate statefulset with the old version

View File

@ -720,6 +720,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
var ( var (
err error err error
initContainers []v1.Container
sidecarContainers []v1.Container sidecarContainers []v1.Container
podTemplate *v1.PodTemplateSpec podTemplate *v1.PodTemplateSpec
volumeClaimTemplate *v1.PersistentVolumeClaim volumeClaimTemplate *v1.PersistentVolumeClaim
@ -740,7 +741,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
limit = c.OpConfig.DefaultMemoryLimit limit = c.OpConfig.DefaultMemoryLimit
} }
isSmaller, err := util.RequestIsSmallerThanLimit(request, limit) isSmaller, err := util.IsSmallerQuantity(request, limit)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -767,7 +768,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
limit = c.OpConfig.DefaultMemoryLimit limit = c.OpConfig.DefaultMemoryLimit
} }
isSmaller, err := util.RequestIsSmallerThanLimit(sidecarRequest, sidecarLimit) isSmaller, err := util.IsSmallerQuantity(sidecarRequest, sidecarLimit)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -786,6 +787,13 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
return nil, fmt.Errorf("could not generate resource requirements: %v", err) return nil, fmt.Errorf("could not generate resource requirements: %v", err)
} }
if spec.InitContainers != nil && len(spec.InitContainers) > 0 {
if c.OpConfig.EnableInitContainers != nil && !(*c.OpConfig.EnableInitContainers) {
c.logger.Warningf("initContainers specified but disabled in configuration - next statefulset creation would fail")
}
initContainers = spec.InitContainers
}
customPodEnvVarsList := make([]v1.EnvVar, 0) customPodEnvVarsList := make([]v1.EnvVar, 0)
if c.OpConfig.PodEnvironmentConfigMap != "" { if c.OpConfig.PodEnvironmentConfigMap != "" {
@ -872,9 +880,14 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
} }
// generate sidecar containers // generate sidecar containers
if sidecarContainers, err = generateSidecarContainers(sideCars, volumeMounts, defaultResources, if sideCars != nil && len(sideCars) > 0 {
c.OpConfig.SuperUsername, c.credentialSecretName(c.OpConfig.SuperUsername), c.logger); err != nil { if c.OpConfig.EnableSidecars != nil && !(*c.OpConfig.EnableSidecars) {
return nil, fmt.Errorf("could not generate sidecar containers: %v", err) c.logger.Warningf("sidecars specified but disabled in configuration - next statefulset creation would fail")
}
if sidecarContainers, err = generateSidecarContainers(sideCars, volumeMounts, defaultResources,
c.OpConfig.SuperUsername, c.credentialSecretName(c.OpConfig.SuperUsername), c.logger); err != nil {
return nil, fmt.Errorf("could not generate sidecar containers: %v", err)
}
} }
tolerationSpec := tolerations(&spec.Tolerations, c.OpConfig.PodToleration) tolerationSpec := tolerations(&spec.Tolerations, c.OpConfig.PodToleration)
@ -894,7 +907,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
c.labelsSet(true), c.labelsSet(true),
annotations, annotations,
spiloContainer, spiloContainer,
spec.InitContainers, initContainers,
sidecarContainers, sidecarContainers,
&tolerationSpec, &tolerationSpec,
effectiveFSGroup, effectiveFSGroup,
@ -1412,7 +1425,7 @@ func (c *Cluster) generatePodDisruptionBudget() *policybeta1.PodDisruptionBudget
pdbEnabled := c.OpConfig.EnablePodDisruptionBudget pdbEnabled := c.OpConfig.EnablePodDisruptionBudget
// if PodDisruptionBudget is disabled or if there are no DB pods, set the budget to 0. // if PodDisruptionBudget is disabled or if there are no DB pods, set the budget to 0.
if (pdbEnabled != nil && !*pdbEnabled) || c.Spec.NumberOfInstances <= 0 { if (pdbEnabled != nil && !(*pdbEnabled)) || c.Spec.NumberOfInstances <= 0 {
minAvailable = intstr.FromInt(0) minAvailable = intstr.FromInt(0)
} }

View File

@ -3,7 +3,7 @@ package cluster
import ( import (
"reflect" "reflect"
"k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"testing" "testing"

View File

@ -65,6 +65,17 @@ func (c *Cluster) listResources() error {
func (c *Cluster) createStatefulSet() (*appsv1.StatefulSet, error) { func (c *Cluster) createStatefulSet() (*appsv1.StatefulSet, error) {
c.setProcessName("creating statefulset") c.setProcessName("creating statefulset")
// check if it's allowed that spec contains initContainers
if c.Spec.InitContainers != nil && len(c.Spec.InitContainers) > 0 &&
c.OpConfig.EnableInitContainers != nil && !(*c.OpConfig.EnableInitContainers) {
return nil, fmt.Errorf("initContainers specified but disabled in configuration")
}
// check if it's allowed that spec contains sidecars
if c.Spec.Sidecars != nil && len(c.Spec.Sidecars) > 0 &&
c.OpConfig.EnableSidecars != nil && !(*c.OpConfig.EnableSidecars) {
return nil, fmt.Errorf("sidecar containers specified but disabled in configuration")
}
statefulSetSpec, err := c.generateStatefulSet(&c.Spec) statefulSetSpec, err := c.generateStatefulSet(&c.Spec)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not generate statefulset: %v", err) return nil, fmt.Errorf("could not generate statefulset: %v", err)

View File

@ -23,6 +23,7 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
oldStatus := c.Status
c.setSpec(newSpec) c.setSpec(newSpec)
defer func() { defer func() {
@ -34,6 +35,16 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error {
} }
}() }()
if err = c.validateResources(&c.Spec); err != nil {
err = fmt.Errorf("insufficient resource limits specified: %v", err)
if oldStatus.Running() {
c.logger.Warning(err)
err = nil
} else {
return err
}
}
if err = c.initUsers(); err != nil { if err = c.initUsers(); err != nil {
err = fmt.Errorf("could not init users: %v", err) err = fmt.Errorf("could not init users: %v", err)
return err return err

View File

@ -5,7 +5,7 @@ import (
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
"k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
policybeta1 "k8s.io/api/policy/v1beta1" policybeta1 "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
) )

View File

@ -111,7 +111,7 @@ func (c *Controller) initOperatorConfig() {
if c.opConfig.SetMemoryRequestToLimit { if c.opConfig.SetMemoryRequestToLimit {
isSmaller, err := util.RequestIsSmallerThanLimit(c.opConfig.DefaultMemoryRequest, c.opConfig.DefaultMemoryLimit) isSmaller, err := util.IsSmallerQuantity(c.opConfig.DefaultMemoryRequest, c.opConfig.DefaultMemoryLimit)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -120,7 +120,7 @@ func (c *Controller) initOperatorConfig() {
c.opConfig.DefaultMemoryRequest = c.opConfig.DefaultMemoryLimit c.opConfig.DefaultMemoryRequest = c.opConfig.DefaultMemoryLimit
} }
isSmaller, err = util.RequestIsSmallerThanLimit(c.opConfig.ScalyrMemoryRequest, c.opConfig.ScalyrMemoryLimit) isSmaller, err = util.IsSmallerQuantity(c.opConfig.ScalyrMemoryRequest, c.opConfig.ScalyrMemoryLimit)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -54,6 +54,8 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.WatchedNamespace = fromCRD.Kubernetes.WatchedNamespace result.WatchedNamespace = fromCRD.Kubernetes.WatchedNamespace
result.PDBNameFormat = fromCRD.Kubernetes.PDBNameFormat result.PDBNameFormat = fromCRD.Kubernetes.PDBNameFormat
result.EnablePodDisruptionBudget = fromCRD.Kubernetes.EnablePodDisruptionBudget result.EnablePodDisruptionBudget = fromCRD.Kubernetes.EnablePodDisruptionBudget
result.EnableInitContainers = fromCRD.Kubernetes.EnableInitContainers
result.EnableSidecars = fromCRD.Kubernetes.EnableSidecars
result.SecretNameTemplate = fromCRD.Kubernetes.SecretNameTemplate result.SecretNameTemplate = fromCRD.Kubernetes.SecretNameTemplate
result.OAuthTokenSecretName = fromCRD.Kubernetes.OAuthTokenSecretName result.OAuthTokenSecretName = fromCRD.Kubernetes.OAuthTokenSecretName
result.InfrastructureRolesSecretName = fromCRD.Kubernetes.InfrastructureRolesSecretName result.InfrastructureRolesSecretName = fromCRD.Kubernetes.InfrastructureRolesSecretName

View File

@ -9,7 +9,7 @@ import (
"github.com/zalando/postgres-operator/pkg/spec" "github.com/zalando/postgres-operator/pkg/spec"
"github.com/zalando/postgres-operator/pkg/util/k8sutil" "github.com/zalando/postgres-operator/pkg/util/k8sutil"
"k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )

View File

@ -123,6 +123,8 @@ type Config struct {
ReplicaDNSNameFormat StringTemplate `name:"replica_dns_name_format" default:"{cluster}-repl.{team}.{hostedzone}"` ReplicaDNSNameFormat StringTemplate `name:"replica_dns_name_format" default:"{cluster}-repl.{team}.{hostedzone}"`
PDBNameFormat StringTemplate `name:"pdb_name_format" default:"postgres-{cluster}-pdb"` PDBNameFormat StringTemplate `name:"pdb_name_format" default:"postgres-{cluster}-pdb"`
EnablePodDisruptionBudget *bool `name:"enable_pod_disruption_budget" default:"true"` EnablePodDisruptionBudget *bool `name:"enable_pod_disruption_budget" default:"true"`
EnableInitContainers *bool `name:"enable_init_containers" default:"true"`
EnableSidecars *bool `name:"enable_sidecars" default:"true"`
Workers uint32 `name:"workers" default:"4"` Workers uint32 `name:"workers" default:"4"`
APIPort int `name:"api_port" default:"8080"` APIPort int `name:"api_port" default:"8080"`
RingLogLines int `name:"ring_log_lines" default:"100"` RingLogLines int `name:"ring_log_lines" default:"100"`

View File

@ -24,7 +24,7 @@ var teamsAPItc = []struct {
{`{ {`{
"dn": "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net", "dn": "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net",
"id": "acid", "id": "acid",
"id_name": "ACID", "id_name": "acid",
"team_id": "111222", "team_id": "111222",
"type": "official", "type": "official",
"name": "Acid team name", "name": "Acid team name",
@ -70,7 +70,7 @@ var teamsAPItc = []struct {
&Team{ &Team{
Dn: "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net", Dn: "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net",
ID: "acid", ID: "acid",
TeamName: "ACID", TeamName: "acid",
TeamID: "111222", TeamID: "111222",
Type: "official", Type: "official",
FullName: "Acid team name", FullName: "Acid team name",

View File

@ -141,17 +141,17 @@ func Coalesce(val, defaultVal string) string {
return val return val
} }
// RequestIsSmallerThanLimit : ... // IsSmallerQuantity : checks if first resource is of a smaller quantity than the second
func RequestIsSmallerThanLimit(requestStr, limitStr string) (bool, error) { func IsSmallerQuantity(requestStr, limitStr string) (bool, error) {
request, err := resource.ParseQuantity(requestStr) request, err := resource.ParseQuantity(requestStr)
if err != nil { if err != nil {
return false, fmt.Errorf("could not parse memory request %v : %v", requestStr, err) return false, fmt.Errorf("could not parse request %v : %v", requestStr, err)
} }
limit, err2 := resource.ParseQuantity(limitStr) limit, err2 := resource.ParseQuantity(limitStr)
if err2 != nil { if err2 != nil {
return false, fmt.Errorf("could not parse memory limit %v : %v", limitStr, err2) return false, fmt.Errorf("could not parse limit %v : %v", limitStr, err2)
} }
return request.Cmp(limit) == -1, nil return request.Cmp(limit) == -1, nil

View File

@ -69,7 +69,7 @@ var substringMatch = []struct {
{regexp.MustCompile(`aaaa (\d+) bbbb`), "aaaa 123 bbbb", nil}, {regexp.MustCompile(`aaaa (\d+) bbbb`), "aaaa 123 bbbb", nil},
} }
var requestIsSmallerThanLimitTests = []struct { var requestIsSmallerQuantityTests = []struct {
request string request string
limit string limit string
out bool out bool
@ -155,14 +155,14 @@ func TestMapContains(t *testing.T) {
} }
} }
func TestRequestIsSmallerThanLimit(t *testing.T) { func TestIsSmallerQuantity(t *testing.T) {
for _, tt := range requestIsSmallerThanLimitTests { for _, tt := range requestIsSmallerQuantityTests {
res, err := RequestIsSmallerThanLimit(tt.request, tt.limit) res, err := IsSmallerQuantity(tt.request, tt.limit)
if err != nil { if err != nil {
t.Errorf("RequestIsSmallerThanLimit returned unexpected error: %#v", err) t.Errorf("IsSmallerQuantity returned unexpected error: %#v", err)
} }
if res != tt.out { if res != tt.out {
t.Errorf("RequestIsSmallerThanLimit expected: %#v, got: %#v", tt.out, res) t.Errorf("IsSmallerQuantity expected: %#v, got: %#v", tt.out, res)
} }
} }
} }