diff --git a/Makefile b/Makefile index f42cfe09a..dc1c790fe 100644 --- a/Makefile +++ b/Makefile @@ -79,7 +79,8 @@ scm-source.json: .git tools: 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: @gofmt -l -w -s $(DIRS) diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index ff92bc064..d50a2b431 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -107,10 +107,14 @@ spec: type: object additionalProperties: type: string + enable_init_containers: + type: boolean enable_pod_antiaffinity: type: boolean enable_pod_disruption_budget: type: boolean + enable_sidecars: + type: boolean infrastructure_roles_secret_name: type: string inherited_labels: @@ -298,3 +302,7 @@ spec: pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' scalyr_server_url: type: string + status: + type: object + additionalProperties: + type: string diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index a8c5f2954..198afe119 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -222,7 +222,7 @@ spec: # only the format of the given number. # # 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 # than the corresponding request. memory: @@ -253,7 +253,7 @@ spec: # only the format of the given number. # # 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 # than the corresponding limit. memory: diff --git a/charts/postgres-operator/values-configmap.yaml b/charts/postgres-operator/values-configmap.yaml index 4b12b1091..269142f3f 100644 --- a/charts/postgres-operator/values-configmap.yaml +++ b/charts/postgres-operator/values-configmap.yaml @@ -59,10 +59,14 @@ configKubernetes: # annotations attached to each database pod # 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 enable_pod_antiaffinity: "false" # toggles PDB to set to MinAvailabe 0 or 1 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 # infrastructure_roles_secret_name: postgresql-infrastructure-roles diff --git a/charts/postgres-operator/values.yaml b/charts/postgres-operator/values.yaml index c6f11e493..40cccfb54 100644 --- a/charts/postgres-operator/values.yaml +++ b/charts/postgres-operator/values.yaml @@ -63,10 +63,14 @@ configKubernetes: # 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 enable_pod_antiaffinity: false # toggles PDB to set to MinAvailabe 0 or 1 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 # infrastructure_roles_secret_name: postgresql-infrastructure-roles diff --git a/docker/logical-backup/dump.sh b/docker/logical-backup/dump.sh index 5c2e478a9..78217322b 100755 --- a/docker/logical-backup/dump.sh +++ b/docker/logical-backup/dump.sh @@ -41,8 +41,8 @@ function aws_upload { args=() [[ ! -z "$EXPECTED_SIZE" ]] && args+=("--expected-size=$EXPECTED_SIZE") - [[ ! -z "$LOGICAL_BACKUP_S3_ENDPOINT" ]] && args+=("--endpoint-url=\"$LOGICAL_BACKUP_S3_ENDPOINT\"") - [[ ! "$LOGICAL_BACKUP_S3_SSE" == "" ]] && args+=("--sse=\"$LOGICAL_BACKUP_S3_SSE\"") + [[ ! -z "$LOGICAL_BACKUP_S3_ENDPOINT" ]] && args+=("--endpoint-url=$LOGICAL_BACKUP_S3_ENDPOINT") + [[ ! "$LOGICAL_BACKUP_S3_SSE" == "" ]] && args+=("--sse=$LOGICAL_BACKUP_S3_SSE") aws s3 cp - "$PATH_TO_BACKUP" "${args[@]//\'/}" --debug } diff --git a/docs/administrator.md b/docs/administrator.md index 44686a25e..cbfe3aa40 100644 --- a/docs/administrator.md +++ b/docs/administrator.md @@ -3,6 +3,26 @@ Learn how to configure and manage the Postgres Operator in your Kubernetes (K8s) 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 [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 -### Service account and cluster roles - 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 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 ``` +### Service account and cluster roles + Note that the service account is named `zalando-postgres-operator`. You may have to change the `service_account_name` in the operator ConfigMap and `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 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` By default `postgresql` custom resources can only be listed and changed by @@ -158,7 +172,6 @@ metadata: name: postgres-operator data: toleration: "key:postgres,operator:Exists,effect:NoSchedule" - ... ``` For an OperatorConfiguration resource the toleration should be defined like @@ -173,7 +186,6 @@ configuration: kubernetes: toleration: 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) @@ -251,7 +263,6 @@ metadata: name: postgres-operator data: inherited_labels: application,environment - ... ``` **OperatorConfiguration** @@ -266,7 +277,6 @@ configuration: inherited_labels: - application - environment -... ``` **cluster manifest** @@ -280,7 +290,7 @@ metadata: application: my-app environment: demo spec: -... + ... ``` **network policy** @@ -295,7 +305,6 @@ spec: matchLabels: application: my-app environment: demo -... ``` @@ -318,7 +327,6 @@ metadata: data: # referencing config map with custom settings pod_environment_configmap: postgres-pod-config - ... ``` **OperatorConfiguration** @@ -332,7 +340,6 @@ configuration: kubernetes: # referencing config map with custom settings pod_environment_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 database. -* **Human users** originate from the Teams API that returns a list of the team -members given a team id. The operator differentiates between (a) product teams -that own a particular Postgres cluster and are granted admin rights to maintain -it, and (b) Postgres superuser teams that get the superuser access to all -Postgres databases running in a K8s cluster for the purposes of maintaining and -troubleshooting. +* **Human users** originate from the [Teams API](user.md#teams-api-roles) that +returns a list of the team members given a team id. The operator differentiates +between (a) product teams that own a particular Postgres cluster and are granted +admin rights to maintain it, and (b) Postgres superuser teams that get the +superuser access to all Postgres databases running in a K8s cluster for the +purposes of maintaining and troubleshooting. ## 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 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. 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: -``` +```bash 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 Operator UI on K8s. For local tests you don't need the Ingress resource. -``` +```bash 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 API server, e.g. for minikube it would be `https://192.168.99.100:8443`. -``` +```bash ./run_local.sh ``` diff --git a/docs/developer.md b/docs/developer.md index 10421e5c2..7ea1e361b 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -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. 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 `git describe --tags --always --dirty` and the image is `registry.opensource.zalan.do/acid/postgres-operator` @@ -60,10 +60,10 @@ The binary will be placed into the build directory. ## Deploying self build image -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 -`load docker-image` from [kind](https://kind.sigs.k8s.io/). The following steps -will get you the docker image built and deployed. +The fastest way to run and test your Docker image locally is to reuse the Docker +environment from [minikube](https://github.com/kubernetes/minikube/releases) +or use the `load docker-image` from [kind](https://kind.sigs.k8s.io/). The +following steps will get you the Docker image built and deployed. ```bash # minikube @@ -163,7 +163,7 @@ The operator also supports pprof endpoints listed at the * /debug/pprof/trace 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 specialized debugger for Go, we will use it as an example. To use it you need: diff --git a/docs/index.md b/docs/index.md index c0e78ac32..87b08deb2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,7 +13,7 @@ manages PostgreSQL clusters on Kubernetes (K8s): 2. The operator also watches updates to [its own configuration](../manifests/configmap.yaml) 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 with the new Docker image. diff --git a/docs/quickstart.md b/docs/quickstart.md index 84f9017a9..3c76e0e9f 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -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. +Non-encrypted connections are rejected by default, so set the SSL mode to +require: ```bash export PGPASSWORD=$(kubectl get secret postgres.acid-minimal-cluster.credentials -o 'jsonpath={.data.password}' | base64 -d) +export PGSSLMODE=require psql -U postgres ``` diff --git a/docs/reference/cluster_manifest.md b/docs/reference/cluster_manifest.md index cf522d73d..bf6df681b 100644 --- a/docs/reference/cluster_manifest.md +++ b/docs/reference/cluster_manifest.md @@ -62,7 +62,7 @@ These parameters are grouped directly under the `spec` key in the manifest. field. * **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. * **spiloFSGroup** @@ -124,7 +124,7 @@ These parameters are grouped directly under the `spec` key in the manifest. * **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 issue](https://github.com/docker-library/postgres/issues/416), which could be not enough if PostgreSQL uses parallel workers heavily. If this option is @@ -185,19 +185,19 @@ explanation of `ttl` and `loop_wait` parameters. * **ttl** Patroni `ttl` parameter value, optional. The default is set by the Spilo - docker image. Optional. + Docker image. Optional. * **loop_wait** Patroni `loop_wait` parameter value, optional. The default is set by the - Spilo docker image. Optional. + Spilo Docker image. Optional. * **retry_timeout** Patroni `retry_timeout` parameter value, optional. The default is set by the - Spilo docker image. Optional. + Spilo Docker image. Optional. * **maximum_lag_on_failover** 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** permanent replication slots that Patroni preserves after failover by @@ -320,7 +320,7 @@ defined in the sidecar dictionary: name of the sidecar. Required. * **image** - docker image of the sidecar. Required. + Docker image of the sidecar. Required. * **env** a dictionary of environment variables. Use usual Kubernetes definition diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index e16282fd2..1055d89b6 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -81,15 +81,15 @@ Those are top-level keys, containing both leaf keys and groups. Kubernetes-native DCS). * **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 your own Spilo image from the [github repository](https://github.com/zalando/spilo). * **sidecar_docker_images** - a map of sidecar names to docker images for the containers to run alongside - Spilo. In case of the name conflict with the definition in the cluster - manifest the cluster-specific one is preferred. + a map of sidecar names to Docker images to run with Spilo. In case of the name + conflict with the definition in the cluster manifest the cluster-specific one + is preferred. * **enable_shm_volume** 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. 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** a template for the name of the database user secrets generated by the operator. `{username}` is replaced with name of the secret, `{cluster}` with diff --git a/docs/user.md b/docs/user.md index ee8e7183c..45f345c87 100644 --- a/docs/user.md +++ b/docs/user.md @@ -13,7 +13,7 @@ kind: postgresql metadata: name: acid-minimal-cluster spec: - teamId: "ACID" + teamId: "acid" volume: size: 1Gi numberOfInstances: 2 @@ -30,7 +30,7 @@ spec: databases: foo: zalando postgresql: - version: "10" + version: "11" ``` 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 ``` +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 ```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 `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 export PGPASSWORD=$(kubectl get secret postgres.acid-minimal-cluster.credentials -o 'jsonpath={.data.password}' | base64 -d) +export PGSSLMODE=require 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 in the manifest. * `infrastructure roles`: create application roles that should be automatically -created on every - cluster managed by the operator. +created on every cluster managed by the operator. * `teams API roles`: automatically create users for every member of the team 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): ```yaml - user1: ZGJ1c2Vy - password1: c2VjcmV0 - inrole1: b3BlcmF0b3I= +user1: ZGJ1c2Vy +password1: c2VjcmV0 +inrole1: b3BlcmF0b3I= ``` 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. ```yaml - dbuser: c2VjcmV0 +dbuser: c2VjcmV0 ``` And the role description for that user should be specified in the ConfigMap. ```yaml - data: - dbuser: | - inrole: [operator, admin] # following roles will be assigned to the new user - user_flags: - - createdb - db_parameters: # db parameters, applied for this particular user - log_statement: all +data: + dbuser: | + inrole: [operator, admin] # following roles will be assigned to the new user + user_flags: + - createdb + db_parameters: # db parameters, applied for this particular user + log_statement: all ``` 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) 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: + +``` +/.../ -> ["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 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. ```yaml -apiVersion: "acid.zalan.do/v1" -kind: postgresql -metadata: - name: acid-minimal-cluster spec: - teamId: "ACID" tolerations: - key: postgres operator: Exists @@ -212,11 +263,6 @@ section in the spec. There are two options here: ### Clone directly ```yaml -apiVersion: "acid.zalan.do/v1" -kind: postgresql - -metadata: - name: acid-test-cluster spec: clone: cluster: "acid-batman" @@ -232,11 +278,6 @@ means that you can clone only from clusters within the same namespace. ### Clone from S3 ```yaml -apiVersion: "acid.zalan.do/v1" -kind: postgresql - -metadata: - name: acid-test-cluster spec: clone: 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: ```yaml -apiVersion: "acid.zalan.do/v1" -kind: postgresql -metadata: - name: acid-test-cluster spec: clone: 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 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 - 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 cluster will not be doing any streaming. It will be in standby mode and allow read-only transactions only. @@ -317,13 +354,7 @@ used for log aggregation, monitoring, backups or other tasks. A sidecar can be specified like this: ```yaml -apiVersion: "acid.zalan.do/v1" -kind: postgresql - -metadata: - name: acid-minimal-cluster spec: - ... sidecars: - name: "container-name" 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 `/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 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: ```yaml -apiVersion: "acid.zalan.do/v1" -kind: postgresql - -metadata: - name: acid-minimal-cluster spec: - ... initContainers: - name: "container-name" image: "company/image:tag" @@ -374,18 +403,17 @@ spec: `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 -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 volume description in the cluster manifest and apply the change: -``` -apiVersion: "acid.zalan.do/v1" -kind: postgresql - -metadata: - name: acid-test-cluster +```yaml spec: volume: 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 parameter in the spec section: -``` +```yaml +spec: enableLogicalBackup: true ``` diff --git a/e2e/tests/test_e2e.py b/e2e/tests/test_e2e.py index 3bdfc1c69..c87e81bd3 100644 --- a/e2e/tests/test_e2e.py +++ b/e2e/tests/test_e2e.py @@ -179,20 +179,14 @@ class EndToEndTestCase(unittest.TestCase): # update the cluster-wide image of the logical backup pod image = "test-image-name" - config_patch = { + patch_logical_backup_image = { "configuration": { "logical_backup": { "logical_backup_docker_image": image, } } } - k8s.api.custom_objects_api.patch_namespaced_custom_object( - "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() + k8s.update_config(patch_logical_backup_image) jobs = k8s.get_logical_backup_job().items 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): self.wait_for_logical_backup_job(expected_num_of_jobs=1) - def apply_kustomization(self, path): - subprocess.run(["kubectl", "apply", "-k", path]) + def update_config(self, patch): + 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): subprocess.run(["kubectl", "create", "-f", path]) + def apply_kustomization(self, path): + subprocess.run(["kubectl", "apply", "-k", path]) + if __name__ == '__main__': unittest.main() diff --git a/go.mod b/go.mod index 9012721d1..36686dcf6 100644 --- a/go.mod +++ b/go.mod @@ -3,23 +3,23 @@ module github.com/zalando/postgres-operator go 1.12 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/evanphx/json-patch v4.5.0+incompatible // 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/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d github.com/sirupsen/logrus v1.4.2 - golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c // indirect - golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect - golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 // indirect - golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f // indirect + golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect + golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect + golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect + golang.org/x/tools v0.0.0-20191209225234-22774f7dae43 // 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/apiextensions-apiserver v0.0.0-20191121021419-88daf26ec3b8 - k8s.io/apimachinery v0.0.0-20191121015412-41065c7a8c2a - k8s.io/client-go v11.0.0+incompatible + k8s.io/apiextensions-apiserver v0.0.0-20191204090421-cd61debedab5 + k8s.io/apimachinery v0.0.0-20191203211716-adc6f4cd9e7d + k8s.io/client-go v0.0.0-20191204082520-bc9b51d240b2 k8s.io/code-generator v0.0.0-20191121015212-c4c8f8345c7e ) diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 4625f7c9a..280da9385 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -o errexit set -o nounset diff --git a/hack/verify-codegen.sh b/hack/verify-codegen.sh index 904586d05..68710015e 100755 --- a/hack/verify-codegen.sh +++ b/hack/verify-codegen.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -o errexit set -o nounset diff --git a/manifests/complete-postgres-manifest.yaml b/manifests/complete-postgres-manifest.yaml index f8495caad..ba2315753 100644 --- a/manifests/complete-postgres-manifest.yaml +++ b/manifests/complete-postgres-manifest.yaml @@ -10,7 +10,7 @@ spec: - name: date image: busybox command: [ "/bin/date" ] - teamId: "ACID" + teamId: "acid" volume: size: 1Gi # storageClass: my-sc diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml index 9e2231d83..d80d08960 100644 --- a/manifests/configmap.yaml +++ b/manifests/configmap.yaml @@ -23,11 +23,13 @@ data: # enable_admin_role_for_users: "true" # enable_crd_validation: "true" # enable_database_access: "true" + # enable_init_containers: "true" enable_master_load_balancer: "false" # enable_pod_antiaffinity: "false" # enable_pod_disruption_budget: "true" enable_replica_load_balancer: "false" # enable_shm_volume: "true" + # enable_sidecars: "true" # enable_team_superuser: "false" enable_teams_api: "false" # etcd_host: "" diff --git a/manifests/minimal-postgres-manifest.yaml b/manifests/minimal-postgres-manifest.yaml index 91d297cac..75dfdf07f 100644 --- a/manifests/minimal-postgres-manifest.yaml +++ b/manifests/minimal-postgres-manifest.yaml @@ -4,7 +4,7 @@ metadata: name: acid-minimal-cluster namespace: default spec: - teamId: "ACID" + teamId: "acid" volume: size: 1Gi numberOfInstances: 2 diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 753415a15..bed892dc8 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -83,10 +83,14 @@ spec: type: object additionalProperties: type: string + enable_init_containers: + type: boolean enable_pod_antiaffinity: type: boolean enable_pod_disruption_budget: type: boolean + enable_sidecars: + type: boolean infrastructure_roles_secret_name: type: string inherited_labels: @@ -274,3 +278,7 @@ spec: pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' scalyr_server_url: type: string + status: + type: object + additionalProperties: + type: string diff --git a/manifests/postgresql-operator-default-configuration.yaml b/manifests/postgresql-operator-default-configuration.yaml index 5b1a15b1d..b8af6951e 100644 --- a/manifests/postgresql-operator-default-configuration.yaml +++ b/manifests/postgresql-operator-default-configuration.yaml @@ -26,14 +26,16 @@ configuration: # custom_pod_annotations: # keya: valuea # keyb: valueb + enable_init_containers: true enable_pod_antiaffinity: false enable_pod_disruption_budget: true - # infrastructure_roles_secret_name: postgresql-infrastructure-roles + enable_sidecars: true + # infrastructure_roles_secret_name: "postgresql-infrastructure-roles" # inherited_labels: # - application # - environment # node_readiness_label: - # status: ready + # status: ready oauth_token_secret_name: postgresql-operator pdb_name_format: "postgres-{cluster}-pdb" pod_antiaffinity_topology_key: "kubernetes.io/hostname" diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index 4a578b324..3b0f652ea 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -186,7 +186,7 @@ spec: # only the format of the given number. # # 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 # than the corresponding request. memory: @@ -217,7 +217,7 @@ spec: # only the format of the given number. # # 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 # than the corresponding limit. memory: @@ -325,3 +325,7 @@ spec: type: string subPath: type: string + status: + type: object + additionalProperties: + type: string diff --git a/manifests/standby-manifest.yaml b/manifests/standby-manifest.yaml index e1bcaf104..e5299bc9b 100644 --- a/manifests/standby-manifest.yaml +++ b/manifests/standby-manifest.yaml @@ -4,7 +4,7 @@ metadata: name: acid-standby-cluster namespace: default spec: - teamId: "ACID" + teamId: "acid" volume: size: 1Gi numberOfInstances: 1 diff --git a/pkg/apis/acid.zalan.do/v1/crds.go b/pkg/apis/acid.zalan.do/v1/crds.go index 9ee76103f..75704afde 100644 --- a/pkg/apis/acid.zalan.do/v1/crds.go +++ b/pkg/apis/acid.zalan.do/v1/crds.go @@ -356,7 +356,7 @@ var PostgresCRDResourceValidation = apiextv1beta1.CustomResourceValidation{ "cpu": { 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", - Pattern: "^(\\d+m|\\d+\\.\\d{1,3})$", + Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$", }, "memory": { Type: "string", @@ -372,7 +372,7 @@ var PostgresCRDResourceValidation = apiextv1beta1.CustomResourceValidation{ "cpu": { 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", - Pattern: "^(\\d+m|\\d+\\.\\d{1,3})$", + Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$", }, "memory": { 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": { Type: "boolean", }, "enable_pod_disruption_budget": { Type: "boolean", }, + "enable_sidecars": { + Type: "boolean", + }, "infrastructure_roles_secret_name": { Type: "string", }, @@ -994,6 +1008,14 @@ var OperatorConfigCRDResourceValidation = apiextv1beta1.CustomResourceValidation }, }, }, + "status": { + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, }, }, } diff --git a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go index d97852b2f..f76790ad5 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -50,6 +50,8 @@ type KubernetesMetaConfiguration struct { WatchedNamespace string `json:"watched_namespace,omitempty"` PDBNameFormat config.StringTemplate `json:"pdb_name_format,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"` ClusterDomain string `json:"cluster_domain"` OAuthTokenSecretName spec.NamespacedName `json:"oauth_token_secret_name,omitempty"` diff --git a/pkg/apis/acid.zalan.do/v1/util_test.go b/pkg/apis/acid.zalan.do/v1/util_test.go index cf3b080a5..fc068b322 100644 --- a/pkg/apis/acid.zalan.do/v1/util_test.go +++ b/pkg/apis/acid.zalan.do/v1/util_test.go @@ -180,7 +180,7 @@ var unmarshalCluster = []struct { "name": "acid-testcluster1" }, "spec": { - "teamId": "ACID", + "teamId": "acid", "pod_priority_class_name": "spilo-pod-priority", "volume": { "size": "5Gi", @@ -290,7 +290,7 @@ var unmarshalCluster = []struct { ResourceLimits: ResourceDescription{CPU: "300m", Memory: "3000Mi"}, }, - TeamID: "ACID", + TeamID: "acid", AllowedSourceRanges: []string{"127.0.0.1/32"}, NumberOfInstances: 2, Users: map[string]UserFlags{"zalando": {"superuser", "createdb"}}, @@ -319,7 +319,7 @@ var unmarshalCluster = []struct { }, 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}, // example with teamId set in input { diff --git a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go index 433d37f87..16f5a9d67 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -81,6 +81,16 @@ func (in *KubernetesMetaConfiguration) DeepCopyInto(out *KubernetesMetaConfigura *out = new(bool) **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.InfrastructureRolesSecretName = in.InfrastructureRolesSecretName if in.ClusterLabels != nil { diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 1f8fe203f..0a7377389 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -227,6 +227,10 @@ func (c *Cluster) Create() error { 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} { 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 // (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 @@ -501,6 +543,7 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { c.mu.Lock() defer c.mu.Unlock() + oldStatus := c.Status c.setStatus(acidv1.ClusterStatusUpdating) 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 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 diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 95e83454f..c69c7a076 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -720,6 +720,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef var ( err error + initContainers []v1.Container sidecarContainers []v1.Container podTemplate *v1.PodTemplateSpec volumeClaimTemplate *v1.PersistentVolumeClaim @@ -740,7 +741,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef limit = c.OpConfig.DefaultMemoryLimit } - isSmaller, err := util.RequestIsSmallerThanLimit(request, limit) + isSmaller, err := util.IsSmallerQuantity(request, limit) if err != nil { return nil, err } @@ -767,7 +768,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef limit = c.OpConfig.DefaultMemoryLimit } - isSmaller, err := util.RequestIsSmallerThanLimit(sidecarRequest, sidecarLimit) + isSmaller, err := util.IsSmallerQuantity(sidecarRequest, sidecarLimit) if err != nil { 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) } + 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) if c.OpConfig.PodEnvironmentConfigMap != "" { @@ -872,9 +880,14 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef } // generate sidecar containers - 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) + if sideCars != nil && len(sideCars) > 0 { + if c.OpConfig.EnableSidecars != nil && !(*c.OpConfig.EnableSidecars) { + 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) @@ -894,7 +907,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef c.labelsSet(true), annotations, spiloContainer, - spec.InitContainers, + initContainers, sidecarContainers, &tolerationSpec, effectiveFSGroup, @@ -1412,7 +1425,7 @@ func (c *Cluster) generatePodDisruptionBudget() *policybeta1.PodDisruptionBudget pdbEnabled := c.OpConfig.EnablePodDisruptionBudget // 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) } diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index 5b206f760..e8fe05456 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -3,7 +3,7 @@ package cluster import ( "reflect" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "testing" diff --git a/pkg/cluster/resources.go b/pkg/cluster/resources.go index 3e8f73916..c94a7bb46 100644 --- a/pkg/cluster/resources.go +++ b/pkg/cluster/resources.go @@ -65,6 +65,17 @@ func (c *Cluster) listResources() error { func (c *Cluster) createStatefulSet() (*appsv1.StatefulSet, error) { 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) if err != nil { return nil, fmt.Errorf("could not generate statefulset: %v", err) diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index dd55cd04c..abe579fb5 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -23,6 +23,7 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error { c.mu.Lock() defer c.mu.Unlock() + oldStatus := c.Status c.setSpec(newSpec) 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 { err = fmt.Errorf("could not init users: %v", err) return err diff --git a/pkg/cluster/types.go b/pkg/cluster/types.go index afdc7376e..138b7015c 100644 --- a/pkg/cluster/types.go +++ b/pkg/cluster/types.go @@ -5,7 +5,7 @@ import ( acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" appsv1 "k8s.io/api/apps/v1" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" policybeta1 "k8s.io/api/policy/v1beta1" "k8s.io/apimachinery/pkg/types" ) diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 9db03ceb1..831078f3e 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -111,7 +111,7 @@ func (c *Controller) initOperatorConfig() { 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 { panic(err) } @@ -120,7 +120,7 @@ func (c *Controller) initOperatorConfig() { 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 { panic(err) } diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 9bed7ed13..56ba91d02 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -54,6 +54,8 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.WatchedNamespace = fromCRD.Kubernetes.WatchedNamespace result.PDBNameFormat = fromCRD.Kubernetes.PDBNameFormat result.EnablePodDisruptionBudget = fromCRD.Kubernetes.EnablePodDisruptionBudget + result.EnableInitContainers = fromCRD.Kubernetes.EnableInitContainers + result.EnableSidecars = fromCRD.Kubernetes.EnableSidecars result.SecretNameTemplate = fromCRD.Kubernetes.SecretNameTemplate result.OAuthTokenSecretName = fromCRD.Kubernetes.OAuthTokenSecretName result.InfrastructureRolesSecretName = fromCRD.Kubernetes.InfrastructureRolesSecretName diff --git a/pkg/controller/util_test.go b/pkg/controller/util_test.go index c9e16cbd9..a5d3c7ac5 100644 --- a/pkg/controller/util_test.go +++ b/pkg/controller/util_test.go @@ -9,7 +9,7 @@ import ( "github.com/zalando/postgres-operator/pkg/spec" "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" ) diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 52a0c4020..d46cba2b2 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -123,6 +123,8 @@ type Config struct { ReplicaDNSNameFormat StringTemplate `name:"replica_dns_name_format" default:"{cluster}-repl.{team}.{hostedzone}"` PDBNameFormat StringTemplate `name:"pdb_name_format" default:"postgres-{cluster}-pdb"` 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"` APIPort int `name:"api_port" default:"8080"` RingLogLines int `name:"ring_log_lines" default:"100"` diff --git a/pkg/util/teams/teams_test.go b/pkg/util/teams/teams_test.go index 637c4e16c..51bbcbc31 100644 --- a/pkg/util/teams/teams_test.go +++ b/pkg/util/teams/teams_test.go @@ -24,7 +24,7 @@ var teamsAPItc = []struct { {`{ "dn": "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net", "id": "acid", -"id_name": "ACID", +"id_name": "acid", "team_id": "111222", "type": "official", "name": "Acid team name", @@ -70,7 +70,7 @@ var teamsAPItc = []struct { &Team{ Dn: "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net", ID: "acid", - TeamName: "ACID", + TeamName: "acid", TeamID: "111222", Type: "official", FullName: "Acid team name", diff --git a/pkg/util/util.go b/pkg/util/util.go index a8ef460db..ad6de14a2 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -141,17 +141,17 @@ func Coalesce(val, defaultVal string) string { return val } -// RequestIsSmallerThanLimit : ... -func RequestIsSmallerThanLimit(requestStr, limitStr string) (bool, error) { +// IsSmallerQuantity : checks if first resource is of a smaller quantity than the second +func IsSmallerQuantity(requestStr, limitStr string) (bool, error) { request, err := resource.ParseQuantity(requestStr) 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) 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 diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index a34e57e23..1f86ea1b4 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -69,7 +69,7 @@ var substringMatch = []struct { {regexp.MustCompile(`aaaa (\d+) bbbb`), "aaaa 123 bbbb", nil}, } -var requestIsSmallerThanLimitTests = []struct { +var requestIsSmallerQuantityTests = []struct { request string limit string out bool @@ -155,14 +155,14 @@ func TestMapContains(t *testing.T) { } } -func TestRequestIsSmallerThanLimit(t *testing.T) { - for _, tt := range requestIsSmallerThanLimitTests { - res, err := RequestIsSmallerThanLimit(tt.request, tt.limit) +func TestIsSmallerQuantity(t *testing.T) { + for _, tt := range requestIsSmallerQuantityTests { + res, err := IsSmallerQuantity(tt.request, tt.limit) if err != nil { - t.Errorf("RequestIsSmallerThanLimit returned unexpected error: %#v", err) + t.Errorf("IsSmallerQuantity returned unexpected error: %#v", err) } if res != tt.out { - t.Errorf("RequestIsSmallerThanLimit expected: %#v, got: %#v", tt.out, res) + t.Errorf("IsSmallerQuantity expected: %#v, got: %#v", tt.out, res) } } }