This commit is contained in:
Rafia Sabih 2019-06-14 12:14:17 +02:00
commit 14570dbc02
45 changed files with 1756 additions and 245 deletions

3
.flake8 Normal file
View File

@ -0,0 +1,3 @@
[flake8]
exclude=.git,__pycache__
max-line-length=120

56
.gitignore vendored
View File

@ -34,3 +34,59 @@ scm-source.json
# diagrams
*.aux
*.log
# Python
# Adapted from https://github.com/github/gitignore/blob/master/Python.gitignore
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot

View File

@ -15,8 +15,9 @@ before_install:
- go get github.com/mattn/goveralls
install:
- make deps
- make deps e2e-tools e2e-build
script:
- hack/verify-codegen.sh
- travis_wait 20 goveralls -service=travis-ci -package ./pkg/... -v
- make e2e-run

View File

@ -1,4 +1,4 @@
.PHONY: clean local test linux macos docker push scm-source.json
.PHONY: clean local test linux macos docker push scm-source.json e2e-run e2e-tools e2e-build
BINARY ?= postgres-operator
BUILD_FLAGS ?= -v
@ -34,7 +34,7 @@ ifdef CDP_PULL_REQUEST_NUMBER
CDP_TAG := -${CDP_BUILD_VERSION}
endif
KIND_PATH := $(GOPATH)/bin
PATH := $(GOPATH)/bin:$(PATH)
SHELL := env PATH=$(PATH) $(SHELL)
@ -91,3 +91,16 @@ deps:
test:
hack/verify-codegen.sh
@go test ./...
e2e-build:
docker build --tag="postgres-operator-e2e-tests" -f e2e/Dockerfile .
e2e-tools:
# install pinned version of 'kind'
# leave the name as is to avoid overwriting official binary named `kind`
wget https://github.com/kubernetes-sigs/kind/releases/download/v0.3.0/kind-linux-amd64
chmod +x kind-linux-amd64
mv kind-linux-amd64 $(KIND_PATH)
e2e-run: docker
e2e/run.sh

View File

@ -1,13 +1,18 @@
apiVersion: v1
name: postgres-operator
version: 0.1.0
appVersion: 1.1.0
version: 1.2.0
appVersion: 1.2.0
home: https://github.com/zalando/postgres-operator
description: Postgres operator creates and manages PostgreSQL clusters running in Kubernetes
keywords:
- postgres
- operator
- cloud-native
- patroni
- spilo
maintainers:
- name: Zalando
email: opensource@zalando.de
- name: kimxogus
email: kgyoo8232@gmail.com
sources:

View File

@ -2,7 +2,11 @@
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: {{ template "postgres-operator.fullname" . }}
name: {{- if eq .Values.serviceAccount.name "" }}
{{ template "postgres-operator.fullname" . }}
{{- else }}
{{ .Values.serviceAccount.name }}
{{- end }}
labels:
app.kubernetes.io/name: {{ template "postgres-operator.name" . }}
helm.sh/chart: {{ template "postgres-operator.chart" . }}
@ -24,6 +28,8 @@ rules:
verbs:
- create
- get
- patch
- update
- apiGroups:
- ""
resources:
@ -138,5 +144,20 @@ rules:
verbs:
- bind
resourceNames:
- {{ template "postgres-operator.fullname" . }}
- {{- if eq .Values.serviceAccount.name "" }}
{{ template "postgres-operator.fullname" . }}
{{- else }}
{{ .Values.serviceAccount.name }}
{{- end }}
- apiGroups:
- batch
resources:
- cronjobs # enables logical backups
verbs:
- create
- delete
- get
- list
- patch
- update
{{ end }}

View File

@ -2,7 +2,11 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ template "postgres-operator.fullname" . }}
name: {{- if eq .Values.serviceAccount.name "" }}
{{ template "postgres-operator.fullname" . }}
{{- else }}
{{ .Values.serviceAccount.name }}
{{- end }}
labels:
app.kubernetes.io/name: {{ template "postgres-operator.name" . }}
helm.sh/chart: {{ template "postgres-operator.chart" . }}
@ -11,11 +15,19 @@ metadata:
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ template "postgres-operator.fullname" . }}
name: {{- if eq .Values.serviceAccount.name "" }}
{{ template "postgres-operator.fullname" . }}
{{- else }}
{{ .Values.serviceAccount.name }}
{{- end }}
subjects:
- kind: ServiceAccount
# note: the cluster role binding needs to be defined
# for every namespace the operator service account lives in.
name: {{ template "postgres-operator.fullname" . }}
name: {{- if eq .Values.serviceAccount.name "" }}
{{ template "postgres-operator.fullname" . }}
{{- else }}
{{ .Values.serviceAccount.name }}
{{- end }}
namespace: {{ .Release.Namespace }}
{{ end }}

View File

@ -8,5 +8,25 @@ metadata:
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/instance: {{ .Release.Name }}
data:
pod_service_account_name: {{ template "postgres-operator.fullname" . }}
{{ toYaml .Values.config | indent 2 }}
pod_service_account_name: {{- if eq .Values.serviceAccount.name "" }}
{{ template "postgres-operator.fullname" . }}
{{- else }}
{{ .Values.serviceAccount.name }}
{{- end }}
api_port: "{{ .Values.configLoggingRestApi.api_port }}"
cluster_history_entries: "{{ .Values.configLoggingRestApi.cluster_history_entries }}"
docker_image: {{ .Values.docker_image }}
debug_logging: "{{ .Values.configDebug.debug_logging }}"
enable_database_access: "{{ .Values.configDebug.enable_database_access }}"
repair_period: {{ .Values.repair_period }}
resync_period: {{ .Values.resync_period }}
ring_log_lines: "{{ .Values.configLoggingRestApi.ring_log_lines }}"
spilo_privileged: "{{ .Values.spilo_privileged }}"
workers: "{{ .Values.workers }}"
{{ toYaml .Values.configMap | indent 2 }}
{{ toYaml .Values.configUsers | indent 2 }}
{{ toYaml .Values.configKubernetes | indent 2 }}
{{ toYaml .Values.configTimeouts | indent 2 }}
{{ toYaml .Values.configLoadBalancer | indent 2 }}
{{ toYaml .Values.configAwsOrGcp | indent 2 }}
{{ toYaml .Values.configTeamsApi | indent 2 }}

View File

@ -0,0 +1,39 @@
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: postgresqls.acid.zalan.do
annotations:
"helm.sh/hook": crd-install
spec:
group: acid.zalan.do
names:
kind: postgresql
listKind: postgresqlList
plural: postgresqls
singular: postgresql
shortNames:
- pg
scope: Namespaced
subresources:
status: {}
version: v1
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: operatorconfigurations.acid.zalan.do
annotations:
"helm.sh/hook": crd-install
spec:
group: acid.zalan.do
names:
kind: OperatorConfiguration
listKind: OperatorConfigurationList
plural: operatorconfigurations
singular: operatorconfiguration
shortNames:
- pgc
scope: Namespaced
subresources:
status: {}
version: v1

View File

@ -17,6 +17,8 @@ spec:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
# In order to use the checksum of CRD OperatorConfiguration instead, use the following line instead
# {{ include (print $.Template.BasePath "/operatorconfiguration.yaml") . | sha256sum }}
{{- if .Values.podAnnotations }}
{{ toYaml .Values.podAnnotations | indent 8 }}
{{- end }}
@ -27,7 +29,11 @@ spec:
{{ toYaml .Values.podLabels | indent 8 }}
{{- end }}
spec:
serviceAccountName: {{ template "postgres-operator.fullname" . }}
serviceAccountName: {{- if eq .Values.serviceAccount.name "" }}
{{ template "postgres-operator.fullname" . }}
{{- else }}
{{ .Values.serviceAccount.name }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}"
@ -35,6 +41,9 @@ spec:
env:
- name: CONFIG_MAP_NAME
value: {{ template "postgres-operator.fullname" . }}
# In order to use the CRD OperatorConfiguration instead, uncomment these lines and comment out the two lines above
# - name: POSTGRES_OPERATOR_CONFIGURATION_OBJECT
# value: {{ template "postgres-operator.fullname" . }}
resources:
{{ toYaml .Values.resources | indent 10 }}
{{- if .Values.imagePullSecrets }}

View File

@ -0,0 +1,41 @@
apiVersion: "acid.zalan.do/v1"
kind: OperatorConfiguration
metadata:
name: {{ template "postgres-operator.fullname" . }}
labels:
app.kubernetes.io/name: {{ template "postgres-operator.name" . }}
helm.sh/chart: {{ template "postgres-operator.chart" . }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/instance: {{ .Release.Name }}
configuration:
docker_image: {{ .Values.docker_image }}
repair_period: {{ .Values.repair_period }}
resync_period: {{ .Values.resync_period }}
workers: {{ .Values.workers }}
{{ toYaml .Values.configCRD | indent 2 }}
users:
{{ toYaml .Values.configUsers | indent 4 }}
kubernetes:
oauth_token_secret_name: {{ template "postgres-operator.fullname" . }}
pod_service_account_name: operator
spilo_privileged: {{ .Values.spilo_privileged }}
{{ toYaml .Values.configKubernetes | indent 4 }}
{{ toYaml .Values.configKubernetesCRD | indent 4 }}
postgres_pod_resources:
{{ toYaml .Values.configPostgresPodResources | indent 4 }}
timeouts:
{{ toYaml .Values.configTimeouts | indent 4 }}
load_balancer:
{{ toYaml .Values.configLoadBalancerCRD | indent 4 }}
aws_or_gcp:
{{ toYaml .Values.configAwsOrGcp | indent 4 }}
logical_backup:
{{ toYaml .Values.configLogicalBackup | indent 4 }}
debug:
{{ toYaml .Values.configDebug | indent 4 }}
teams_api:
{{ toYaml .Values.configTeamsApiCRD | indent 4 }}
logging_rest_api:
{{ toYaml .Values.configLoggingRestApi | indent 4 }}
scalyr:
{{ toYaml .Values.configScalyr | indent 4 }}

View File

@ -2,7 +2,11 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ template "postgres-operator.fullname" . }}
name: {{- if eq .Values.serviceAccount.name "" }}
{{ template "postgres-operator.fullname" . }}
{{- else }}
{{ .Values.serviceAccount.name }}
{{- end }}
labels:
app.kubernetes.io/name: {{ template "postgres-operator.name" . }}
helm.sh/chart: {{ template "postgres-operator.chart" . }}

View File

@ -1,7 +1,7 @@
image:
registry: registry.opensource.zalan.do
repository: acid/postgres-operator
tag: v1.1.0
tag: v1.1.0-28-g24d412a
pullPolicy: "IfNotPresent"
# Optionally specify an array of imagePullSecrets.
@ -13,55 +13,144 @@ image:
podAnnotations: {}
podLabels: {}
config:
watched_namespace: "*" # listen to all namespaces
cluster_labels: application:spilo
cluster_name_label: version
pod_role_label: spilo-role
# config shared from ConfigMap and CRD
docker_image: registry.opensource.zalan.do/acid/spilo-11:1.5-p7
repair_period: 5m
resync_period: 5m
spilo_privileged: false
workers: 4
debug_logging: "true"
workers: "4"
docker_image: registry.opensource.zalan.do/acid/spilo-cdp-11:1.5-p70
secret_name_template: '{username}.{cluster}.credentials'
configUsers:
replication_username: standby
super_username: postgres
enable_teams_api: "false"
spilo_privileged: "false"
# set_memory_request_to_limit: "true"
# postgres_superuser_teams: "postgres_superusers"
# enable_team_superuser: "false"
# team_admin_role: "admin"
# teams_api_url: http://fake-teams-api.default.svc.cluster.local
# team_api_role_configuration: "log_statement:all"
# infrastructure_roles_secret_name: postgresql-infrastructure-roles
# oauth_token_secret_name: postgresql-operator
# pam_role_name: zalandos
# pam_configuration: |
# https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees
configKubernetes:
cluster_domain: cluster.local
# inherited_labels: ""
aws_region: eu-central-1
db_hosted_zone: db.example.com
master_dns_name_format: '{cluster}.{team}.staging.{hostedzone}'
replica_dns_name_format: '{cluster}-repl.{team}.staging.{hostedzone}'
enable_master_load_balancer: "true"
enable_replica_load_balancer: "false"
# infrastructure_roles_secret_name: postgresql-infrastructure-roles
# node_readiness_label: ""
# oauth_token_secret_name: postgresql-operator
# pod_environment_configmap: ""
# spilo_fsgroup: "103"
pod_management_policy: "ordered_ready"
pdb_name_format: "postgres-{cluster}-pdb"
api_port: "8080"
ring_log_lines: "100"
cluster_history_entries: "1000"
pod_role_label: spilo-role
pod_terminate_grace_period: 5m
secret_name_template: '{username}.{cluster}.credentials'
configPostgresPodResources:
default_cpu_request: 100m
default_memory_request: 100Mi
default_cpu_limit: "3"
default_memory_limit: 1Gi
# set_memory_request_to_limit: true
configTimeouts:
# master_pod_move_timeout: 10m
pod_deletion_wait_timeout: 10m
pod_label_wait_timeout: 10m
ready_wait_interval: 3s
ready_wait_timeout: 30s
replication_username: standby
resource_check_interval: 3s
resource_check_timeout: 10m
resync_period: 5m
pod_management_policy: "ordered_ready"
enable_pod_antiaffinity: "false"
configDebug:
debug_logging: true
enable_database_access: true
configLoggingRestApi:
api_port: 8080
cluster_history_entries: 1000
ring_log_lines: 100
configAwsOrGcp:
aws_region: eu-central-1
db_hosted_zone: db.example.com
# kube_iam_role: ""
# log_s3_bucket: ""
# wal_s3_bucket: ""
configLogicalBackup:
logical_backup_schedule: "30 00 * * *"
logical_backup_docker_image: "registry.opensource.zalan.do/acid/logical-backup"
logical_backup_s3_bucket: ""
# config exclusive to ConfigMap
configMap:
cluster_labels: application:spilo
cluster_name_label: version
watched_namespace: "*" # listen to all namespaces
configLoadBalancer:
# custom_service_annotations:
# "keyx:valuez,keya:valuea"
enable_master_load_balancer: "true"
enable_replica_load_balancer: "false"
master_dns_name_format: '{cluster}.{team}.staging.{hostedzone}'
replica_dns_name_format: '{cluster}-repl.{team}.staging.{hostedzone}'
configTeamsApi:
enable_teams_api: "false"
# enable_admin_role_for_users: "true"
# enable_team_superuser: "false"
# pam_configuration: https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees
# pam_role_name: zalandos
# postgres_superuser_teams: "postgres_superusers"
# team_admin_role: "admin"
# team_api_role_configuration: "log_statement:all"
# teams_api_url: http://fake-teams-api.default.svc.cluster.local
# config exclusive to CRD
configCRD:
etcd_host: ""
min_instances: -1
max_instances: -1
# sidecar_docker_images
# example: "exampleimage:exampletag"
configKubernetesCRD:
cluster_labels:
application: spilo
cluster_name_label: cluster-name
enable_pod_antiaffinity: false
pod_antiaffinity_topology_key: "kubernetes.io/hostname"
secret_name_template: "{username}.{cluster}.credentials.{tprkind}.{tprgroup}"
# inherited_labels:
# - application
# - app
# watched_namespace: ""
configLoadBalancerCRD:
# custom_service_annotations:
# keyx: valuez
# keya: valuea
enable_master_load_balancer: false
enable_replica_load_balancer: false
master_dns_name_format: "{cluster}.{team}.{hostedzone}"
replica_dns_name_format: "{cluster}-repl.{team}.{hostedzone}"
configTeamsApiCRD:
enable_teams_api: false
enable_team_superuser: false
# pam_configuration: ""
pam_role_name: zalandos
# postgres_superuser_teams: "postgres_superusers"
protected_role_names:
- admin
team_admin_role: admin
team_api_role_configuration:
log_statement: all
# teams_api_url: ""
scalyr:
scalyr_cpu_request: 100m
scalyr_memory_request: 50Mi
scalyr_cpu_limit: "1"
scalyr_memory_limit: 1Gi
# scalyr_api_key: ""
# scalyr_image: ""
# scalyr_server_url: ""
rbac:
# Specifies whether RBAC resources should be created
create: true
@ -71,7 +160,9 @@ serviceAccount:
create: true
# The name of the ServiceAccount to use.
# If not set and create is true, a name is generated using the fullname template
name:
# When relying solely on the OperatorConfiguration CRD, set this value to "operator"
# Otherwise, the operator tries to use the "default" service account which is forbidden
name: ""
priorityClassName: ""

View File

@ -11,7 +11,7 @@ pipeline:
apt-get update
- desc: 'Install required build software'
cmd: |
apt-get install -y make git apt-transport-https ca-certificates curl build-essential
apt-get install -y make git apt-transport-https ca-certificates curl build-essential python3 python3-pip
- desc: 'Install go'
cmd: |
cd /tmp
@ -41,6 +41,10 @@ pipeline:
export PATH=$PATH:$HOME/go/bin
cd $OPERATOR_TOP_DIR/postgres-operator
go test ./...
- desc: 'Run e2e tests'
cmd: |
cd $OPERATOR_TOP_DIR/postgres-operator
make e2e-tools e2e-build e2e-run
- desc: 'Push docker image'
cmd: |
export PATH=$PATH:$HOME/go/bin

View File

@ -0,0 +1,33 @@
FROM ubuntu:18.04
LABEL maintainer="Team ACID @ Zalando <team-acid@zalando.de>"
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
apt-utils \
ca-certificates \
lsb-release \
pigz \
python3-pip \
python3-setuptools \
curl \
jq \
gnupg \
&& pip3 install --no-cache-dir awscli --upgrade \
&& echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
&& cat /etc/apt/sources.list.d/pgdg.list \
&& curl --silent https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \
&& apt-get update \
&& apt-get install --no-install-recommends -y \
postgresql-client-11 \
postgresql-client-10 \
postgresql-client-9.6 \
postgresql-client-9.5 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY dump.sh ./
ENV PG_DIR=/usr/lib/postgresql/
ENTRYPOINT ["/dump.sh"]

94
docker/logical-backup/dump.sh Executable file
View File

@ -0,0 +1,94 @@
#! /usr/bin/env bash
# enable unofficial bash strict mode
set -o errexit
set -o nounset
set -o pipefail
IFS=$'\n\t'
# make script trace visible via `kubectl logs`
set -o xtrace
ALL_DB_SIZE_QUERY="select sum(pg_database_size(datname)::numeric) from pg_database;"
PG_BIN=$PG_DIR/$PG_VERSION/bin
DUMP_SIZE_COEFF=5
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
K8S_API_URL=https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT/api/v1
CERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
function estimate_size {
"$PG_BIN"/psql -tqAc "${ALL_DB_SIZE_QUERY}"
}
function dump {
# settings are taken from the environment
"$PG_BIN"/pg_dumpall
}
function compress {
pigz
}
function aws_upload {
declare -r EXPECTED_SIZE="$1"
# mimic bucket setup from Spilo
# to keep logical backups at the same path as WAL
# NB: $LOGICAL_BACKUP_S3_BUCKET_SCOPE_SUFFIX already contains the leading "/" when set by the Postgres operator
PATH_TO_BACKUP=s3://$LOGICAL_BACKUP_S3_BUCKET"/spilo/"$SCOPE$LOGICAL_BACKUP_S3_BUCKET_SCOPE_SUFFIX"/logical_backups/"$(date +%s).sql.gz
if [ -z "$EXPECTED_SIZE" ]; then
aws s3 cp - "$PATH_TO_BACKUP" --debug --sse="AES256"
else
aws s3 cp - "$PATH_TO_BACKUP" --debug --expected-size "$EXPECTED_SIZE" --sse="AES256"
fi;
}
function get_pods {
declare -r SELECTOR="$1"
curl "${K8S_API_URL}/pods?$SELECTOR" \
--cacert $CERT \
-H "Authorization: Bearer ${TOKEN}" | jq .items[].status.podIP -r
}
function get_current_pod {
curl "${K8S_API_URL}/pods?fieldSelector=metadata.name%3D${HOSTNAME}" \
--cacert $CERT \
-H "Authorization: Bearer ${TOKEN}"
}
declare -a search_strategy=(
list_all_replica_pods_current_node
list_all_replica_pods_any_node
get_master_pod
)
function list_all_replica_pods_current_node {
get_pods "labelSelector=version%3D${SCOPE},spilo-role%3Dreplica&fieldSelector=spec.nodeName%3D${CURRENT_NODENAME}" | head -n 1
}
function list_all_replica_pods_any_node {
get_pods "labelSelector=version%3D${SCOPE},spilo-role%3Dreplica" | head -n 1
}
function get_master_pod {
get_pods "labelSelector=version%3D${SCOPE},spilo-role%3Dmaster" | head -n 1
}
CURRENT_NODENAME=$(get_current_pod | jq .items[].spec.nodeName --raw-output)
export CURRENT_NODENAME
for search in "${search_strategy[@]}"; do
PGHOST=$(eval "$search")
export PGHOST
if [ -n "$PGHOST" ]; then
break
fi
done
dump | compress | aws_upload $(($(estimate_size) / DUMP_SIZE_COEFF))

View File

@ -1,47 +1,3 @@
## Create ConfigMap
A ConfigMap is used to store the configuration of the operator.
```bash
$ kubectl create -f manifests/configmap.yaml
```
## Deploying the operator
First you need to install the service account definition in your Minikube cluster.
```bash
$ kubectl create -f manifests/operator-service-account-rbac.yaml
```
Next deploy the postgres-operator from the docker image Zalando is using:
```bash
$ kubectl create -f manifests/postgres-operator.yaml
```
If you prefer to build the image yourself follow up down below.
### - Helm chart
You can install postgres-operator also with a [Helm](https://helm.sh/) chart.
This requires installing the Helm CLI first and then initializing it in the
cluster.
```bash
$ helm init
$ helm install --name my-release ./charts/postgres-operator
```
## Check if CustomResourceDefinition has been registered
```bash
$ kubectl get crd
NAME KIND
postgresqls.acid.zalan.do CustomResourceDefinition.v1beta1.apiextensions.k8s.io
```
# How to configure PostgreSQL operator
## Select the namespace to deploy to
@ -103,6 +59,12 @@ In this definition, the operator overwrites the account's name to match
`pod_service_account_name` and the `default` namespace to match the target
namespace. The operator performs **no** further syncing of this account.
## Non-default cluster domain
If your cluster uses a different dns domain than `cluster.local`, this needs
to be set in the operator ConfigMap. This is used by the operator to connect
to the clusters after creation.
## Role-based access control for the operator
The `manifests/operator-service-account-rbac.yaml` defines cluster roles and
@ -340,9 +302,18 @@ Postgres database cluster:
## Understanding rolling update of Spilo pods
The operator logs reasons for a rolling update with the `info` level and
a diff between the old and new StatefulSet specs with the `debug` level.
To read the latter log entry with the escaped characters rendered, view it
in CLI with `echo -e`. Note that the resultant message will contain some
noise because the `PodTemplate` used by the operator is yet to be updated
with the default values used internally in Kubernetes.
The operator logs reasons for a rolling update with the `info` level and a diff between the old and new StatefulSet specs with the `debug` level. To benefit from numerous escape characters in the latter log entry, view it in CLI with `echo -e`. Note that the resultant message will contain some noise because the `PodTemplate` used by the operator is yet to be updated with the default values used internally in Kubernetes.
## Logical backups
The operator can manage k8s cron jobs to run logical backups of Postgres clusters. The cron job periodically spawns a batch job that runs a single pod. The backup script within this pod's container can connect to a DB for a logical backup. The operator updates cron jobs during Sync if the job schedule changes; the job name acts as the job identifier. These jobs are to be enabled for each indvidual Postgres cluster by setting `enableLogicalBackup: true` in its manifest. Notes:
1. The [example image](../docker/logical-backup/Dockerfile) implements the backup via `pg_dumpall` and upload of compressed and encrypted results to an S3 bucket; the default image ``registry.opensource.zalan.do/acid/logical-backup`` is the same image built with the Zalando-internal CI pipeline. `pg_dumpall` requires a `superuser` access to a DB and runs on the replica when possible.
2. Due to the [limitation of Kubernetes cron jobs](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#cron-job-limitations) it is highly advisable to set up additional monitoring for this feature; such monitoring is outside of the scope of operator responsibilities.
3. The operator does not remove old backups.
4. You may use your own image by overwriting the relevant field in the operator configuration. Any such image must ensure the logical backup is able to finish [in presence of pod restarts](https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/#handling-pod-and-container-failures) and [simultaneous invocations](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#cron-job-limitations) of the backup cron job.
5. For that feature to work, your RBAC policy must enable operations on the `cronjobs` resource from the `batch` API group for the operator service account. See [example RBAC](../manifests/operator-service-account-rbac.yaml)

View File

@ -20,18 +20,17 @@ that your setup is working.
Note: if you use multiple Kubernetes clusters, you can switch to Minikube with
`kubectl config use-context minikube`
## Create ConfigMap
## Deploying the operator
ConfigMap is used to store the configuration of the operator
### Kubernetes manifest
A ConfigMap is used to store the configuration of the operator. Alternatively,
a CRD-based configuration can be used, as described [here](reference/operator_parameters).
```bash
$ kubectl --context minikube create -f manifests/configmap.yaml
```
## Deploying the operator
### - Kubernetes manifest
First you need to install the service account definition in your Minikube cluster.
```bash
@ -46,15 +45,23 @@ Next deploy the postgres-operator from the docker image Zalando is using:
If you prefer to build the image yourself follow up down below.
### - Helm chart
### Helm chart
You can install postgres-operator also with a [Helm](https://helm.sh/) chart.
This requires installing the Helm CLI first and then initializing it in the
cluster.
Alternatively, the operator can be installed by using the provided [Helm](https://helm.sh/)
chart which saves you the manual steps. Therefore, you would need to install
the helm CLI on your machine. After initializing helm (and its server
component Tiller) in your local cluster you can install the operator chart.
You can define a release name that is prepended to the operator resource's
names.
Use `--name zalando` to match with the default service account name as older
operator versions do not support custom names for service accounts. When relying
solely on the CRD-based configuration edit the `serviceAccount` section in the
[values yaml file](../charts/values.yaml) by setting the name to `"operator"`.
```bash
$ helm init
$ helm install --name my-release ./charts/postgres-operator
$ helm install --name zalando ./charts/postgres-operator
```
## Check if CustomResourceDefinition has been registered
@ -203,7 +210,7 @@ localhost:8080 by doing:
The inner 'query' gets the name of the postgres operator pod, and the outer
enables port forwarding. Afterwards, you can access the operator API with:
$ curl http://127.0.0.1:8080/$endpoint| jq .
$ curl --location http://127.0.0.1:8080/$endpoint | jq .
The available endpoints are listed below. Note that the worker ID is an integer
from 0 up to 'workers' - 1 (value configured in the operator configuration and
@ -315,6 +322,16 @@ Then you can for example check the Patroni logs:
kubectl logs acid-minimal-cluster-0
```
## End-to-end tests
The operator provides reference e2e (end-to-end) tests to ensure various infra parts work smoothly together.
Each e2e execution tests a Postgres operator image built from the current git branch. The test runner starts a [kind](https://kind.sigs.k8s.io/) (local k8s) cluster and Docker container with tests. The k8s API client from within the container connects to the `kind` cluster using the standard Docker `bridge` network.
The tests utilize examples from `/manifests` (ConfigMap is used for the operator configuration) to avoid maintaining yet another set of configuration files. The kind cluster is deleted if tests complete successfully.
End-to-end tests are executed automatically during builds; to invoke them locally use `make e2e-run` from the project's top directory. Run `make e2e-tools e2e-build` to install `kind` and build the tests' image locally before the first run.
End-to-end tests are written in Python and use `flake8` for code quality. Please run flake8 [before submitting a PR](http://flake8.pycqa.org/en/latest/user/using-hooks.html).
## Introduce additional configuration parameters
In the case you want to add functionality to the operator that shall be
@ -323,6 +340,9 @@ be updated. As explained [here](reference/operator_parameters.md), it's possible
to configure the operator either with a ConfigMap or CRD, but currently we aim
to synchronize parameters everywhere.
When choosing a parameter name for a new option in a PG manifest, keep in mind
the naming conventions there. The `snake_case` variables come from the Patroni/Postgres world, while the `camelCase` from the k8s world.
Note: If one option is defined in the operator configuration and in the cluster
[manifest](../manifests/complete-postgres-manifest.yaml), the latter takes
precedence.

View File

@ -20,6 +20,12 @@ cd postgres-operator
minikube start
```
If you want to configure the Postgres Operator it must happen before deploying a
Postgres cluster. This can happen in two ways: Via a ConfigMap or a
`OperatorConfiguration` object, which adheres a CustomResourceDefinition (CRD).
More details on configuration can be found [here](reference/operator_parameters.md).
## Manual deployment setup
The Postgres Operator can be installed simply by applying yaml manifests.
@ -37,8 +43,12 @@ chart which saves you the manual steps. Therefore, you would need to install
the helm CLI on your machine. After initializing helm (and its server
component Tiller) in your local cluster you can install the operator chart.
You can define a release name that is prepended to the operator resource's
names. Use `--name zalando` to match with the default service account name
as older operator versions do not support custom names for service accounts.
names.
Use `--name zalando` to match with the default service account name as older
operator versions do not support custom names for service accounts. When relying
solely on the CRD-based configuration edit the `serviceAccount` section in the
[values yaml file](../charts/values.yaml) by setting the name to `"operator"`.
```bash
# 1) initialize helm

View File

@ -1,4 +1,3 @@
Individual postgres clusters are described by the Kubernetes *cluster manifest*
that has the structure defined by the `postgres CRD` (custom resource
definition). The following section describes the structure of the manifest and
@ -14,6 +13,10 @@ measurements. Please, refer to the [Kubernetes
documentation](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/)
for the possible values of those.
:exclamation: If both operator configmap/CRD and a Postgres cluster manifest
define the same parameter, the value from the Postgres cluster manifest is
applied.
## Manifest structure
A postgres manifest is a `YAML` document. On the top level both individual
@ -45,7 +48,7 @@ Those parameters are grouped under the `metadata` top-level key.
## Top-level parameters
Those are parameters grouped directly under the `spec` key in the manifest.
These parameters are grouped directly under the `spec` key in the manifest.
* **teamId**
name of the team the cluster belongs to. Changing it after the cluster
@ -55,6 +58,13 @@ Those are parameters grouped directly under the `spec` key in the manifest.
custom docker image that overrides the **docker_image** operator parameter.
It should be a [Spilo](https://github.com/zalando/spilo) image. Optional.
* **spiloFSGroup**
the Persistent Volumes for the spilo pods in the StatefulSet will be owned
and writable by the group ID specified. This will override the **spilo_fsgroup**
operator parameter. This is required to run Spilo as a non-root process, but
requires a custom spilo image. Note the FSGroup of a Pod cannot be changed
without recreating a new Pod.
* **enableMasterLoadBalancer**
boolean flag to override the operator defaults (set by the
`enable_master_load_balancer` parameter) to define whether to enable the load
@ -103,7 +113,8 @@ Those are parameters grouped directly under the `spec` key in the manifest.
class](https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass)
that should be assigned to the cluster pods. When not specified, the value
is taken from the `pod_priority_class_name` operator parameter, if not set
then the default priority class is taken. The priority class itself must be defined in advance.
then the default priority class is taken. The priority class itself must be
defined in advance.
* **enableShmVolume**
Start a database pod without limitations on shm memory. By default docker
@ -117,6 +128,14 @@ Those are parameters grouped directly under the `spec` key in the manifest.
is `false`, then no volume will be mounted no matter how operator was
configured (so you can override the operator configuration).
* **enableLogicalBackup**
Determines if the logical backup of this cluster should be taken and uploaded
to S3. Default: false.
* **logicalBackupSchedule**
Schedule for the logical backup k8s cron job. Please take [the reference schedule format](https://kubernetes.io/docs/tasks/job/automated-tasks-with-cron-jobs/#schedule)
into account. Default: "30 00 \* \* \*"
## Postgres parameters
Those parameters are grouped under the `postgresql` top-level key.
@ -173,7 +192,12 @@ explanation of `ttl` and `loop_wait` parameters.
set by the Spilo docker image. Optional.
* **slots**
permanent replication slots that Patroni preserves after failover by re-creating them on the new primary immediately after doing a promote. Slots could be reconfigured with the help of `patronictl edit-config`. It is the responsibility of a user to avoid clashes in names between replication slots automatically created by Patroni for cluster members and permanent replication slots. Optional.
permanent replication slots that Patroni preserves after failover by
re-creating them on the new primary immediately after doing a promote. Slots
could be reconfigured with the help of `patronictl edit-config`. It is the
responsibility of a user to avoid clashes in names between replication slots
automatically created by Patroni for cluster members and permanent replication
slots. Optional.
* **standby_cluster**
initializes cluster as a standby creating a cascading replication, where standby leader is streaming from specified remote location
@ -265,3 +289,36 @@ defined in the sidecar dictionary:
a dictionary of environment variables. Use usual Kubernetes definition
(https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/)
for environment variables. Optional.
* **resources** see below. Optional.
#### Sidecar container resources
Those parameters define [CPU and memory requests and
limits](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/)
for the sidecar container. They are grouped under the `resources` key for each sidecar.
There are two subgroups, `requests` and `limits`.
##### Requests
CPU and memory requests for the sidecar container.
* **cpu**
CPU requests for the sidecar container. Optional, overrides the
`default_cpu_requests` operator configuration parameter. Optional.
* **memory**
memory requests for the sidecar container. Optional, overrides the
`default_memory_request` operator configuration parameter. Optional.
##### Limits
CPU and memory limits for the sidecar container.
* **cpu**
CPU limits for the sidecar container. Optional, overrides the
`default_cpu_limits` operator configuration parameter. Optional.
* **memory**
memory limits for the sidecar container. Optional, overrides the
`default_memory_limits` operator configuration parameter. Optional.

View File

@ -11,17 +11,18 @@ configuration.
[example](https://github.com/zalando/postgres-operator/blob/master/manifests/configmap.yaml)
* CRD-based configuration. The configuration is stored in a custom YAML
manifest. The manifest is an instance of the custom resource definition (CRD) called
`OperatorConfiguration`. The operator registers this CRD
during the start and uses it for configuration if the [operator deployment manifest ](https://github.com/zalando/postgres-operator/blob/master/manifests/postgres-operator.yaml#L21) sets the `POSTGRES_OPERATOR_CONFIGURATION_OBJECT` env variable to a non-empty value. The variable should point to the
`postgresql-operator-configuration` object in the operator's namespace.
manifest. The manifest is an instance of the custom resource definition (CRD)
called `OperatorConfiguration`. The operator registers this CRD during the
start and uses it for configuration if the [operator deployment manifest ](https://github.com/zalando/postgres-operator/blob/master/manifests/postgres-operator.yaml#L21)
sets the `POSTGRES_OPERATOR_CONFIGURATION_OBJECT` env variable to a non-empty
value. The variable should point to the `postgresql-operator-configuration`
object in the operator's namespace.
The CRD-based configuration is a regular YAML
document; non-scalar keys are simply represented in the usual YAML way.
There are no default values built-in in the operator, each parameter that is
not supplied in the configuration receives an empty value. In order to
create your own configuration just copy the [default
one](https://github.com/zalando/postgres-operator/blob/master/manifests/postgresql-operator-default-configuration.yaml)
The CRD-based configuration is a regular YAML document; non-scalar keys are
simply represented in the usual YAML way. There are no default values built-in
in the operator, each parameter that is not supplied in the configuration
receives an empty value. In order to create your own configuration just copy
the [default one](https://github.com/zalando/postgres-operator/blob/master/manifests/postgresql-operator-default-configuration.yaml)
and change it.
To test the CRD-based configuration locally, use the following
@ -31,19 +32,23 @@ configuration.
kubectl create -f manifests/postgresql-operator-default-configuration.yaml
kubectl get operatorconfigurations postgresql-operator-default-configuration -o yaml
```
Note that the operator first registers the definition of the CRD `OperatorConfiguration` and then waits for an instance of the CRD to be created. In between these two event the operator pod may be failing since it cannot fetch the not-yet-existing `OperatorConfiguration` instance.
Note that the operator first registers the CRD of the `OperatorConfiguration`
and then waits for an instance to be created. In between these two event the
operator pod may be failing since it cannot fetch the not-yet-existing
`OperatorConfiguration` instance.
The CRD-based configuration is more powerful than the one based on
ConfigMaps and should be used unless there is a compatibility requirement to
use an already existing configuration. Even in that case, it should be rather
straightforward to convert the configmap based configuration into the CRD-based
one and restart the operator. The ConfigMaps-based configuration will be
deprecated and subsequently removed in future releases.
The CRD-based configuration is more powerful than the one based on ConfigMaps
and should be used unless there is a compatibility requirement to use an already
existing configuration. Even in that case, it should be rather straightforward
to convert the configmap based configuration into the CRD-based one and restart
the operator. The ConfigMaps-based configuration will be deprecated and
subsequently removed in future releases.
Note that for the CRD-based configuration groups of configuration options below correspond
to the non-leaf keys in the target YAML (i.e. for the Kubernetes resources the
key is `kubernetes`). The key is mentioned alongside the group description. The
ConfigMap-based configuration is flat and does not allow non-leaf keys.
Note that for the CRD-based configuration groups of configuration options below
correspond to the non-leaf keys in the target YAML (i.e. for the Kubernetes
resources the key is `kubernetes`). The key is mentioned alongside the group
description. The ConfigMap-based configuration is flat and does not allow
non-leaf keys.
Since in the CRD-based case the operator needs to create a CRD first, which is
controlled by the `resource_check_interval` and `resource_check_timeout`
@ -51,6 +56,12 @@ parameters, those parameters have no effect and are replaced by the
`CRD_READY_WAIT_INTERVAL` and `CRD_READY_WAIT_TIMEOUT` environment variables.
They will be deprecated and removed in the future.
For the configmap operator configuration, the [default parameter values](https://github.com/zalando-incubator/postgres-operator/blob/master/pkg/util/config/config.go#L14)
mentioned here are likely to be overwritten in your local operator installation
via your local version of the operator configmap. In the case you use the
operator CRD, all the CRD defaults are provided in the
[operator's default configuration manifest](https://github.com/zalando-incubator/postgres-operator/blob/master/manifests/postgresql-operator-default-configuration.yaml)
Variable names are underscore-separated words.
@ -85,8 +96,8 @@ Those are top-level keys, containing both leaf keys and groups.
* **min_instances**
operator will run at least the number of instances for any given postgres
cluster equal to the value of this parameter. When `-1` is specified, no limits
are applied. The default is `-1`.
cluster equal to the value of this parameter. When `-1` is specified, no
limits are applied. The default is `-1`.
* **resync_period**
period between consecutive sync requests. The default is `30m`.
@ -122,7 +133,8 @@ configuration they are grouped under the `kubernetes` key.
* **pod_service_account_definition**
The operator tries to create the pod Service Account in the namespace that
doesn't define such an account using the YAML definition provided by this
option. If not defined, a simple definition that contains only the name will be used. The default is empty.
option. If not defined, a simple definition that contains only the name will
be used. The default is empty.
* **pod_service_account_role_binding_definition**
This definition must bind pod service account to a role with permission
@ -140,8 +152,8 @@ configuration they are grouped under the `kubernetes` key.
* **watched_namespace**
The operator watches for postgres objects in the given namespace. If not
specified, the value is taken from the operator namespace. A special `*`
value makes it watch all namespaces. The default is empty (watch the operator pod
namespace).
value makes it watch all namespaces. The default is empty (watch the operator
pod namespace).
* **pdb_name_format**
defines the template for PDB (Pod Disruption Budget) names created by the
@ -157,6 +169,11 @@ configuration they are grouped under the `kubernetes` key.
allowed. The default is
`{username}.{cluster}.credentials.{tprkind}.{tprgroup}`.
* **cluster_domain**
defines the default dns domain for the kubernetes cluster the operator is
running in. The default is `cluster.local`. Used by the operator to connect
to the postgres clusters after creation.
* **oauth_token_secret_name**
a name of the secret containing the `OAuth2` token to pass to the teams API.
The default is `postgresql-operator`.
@ -174,8 +191,8 @@ configuration they are grouped under the `kubernetes` key.
* **inherited_labels**
list of labels that can be inherited from the cluster manifest, and added to
each child objects (`StatefulSet`, `Pod`, `Service` and `Endpoints`) created by
the opertor.
each child objects (`StatefulSet`, `Pod`, `Service` and `Endpoints`) created
by the opertor.
Typical use case is to dynamically pass labels that are specific to a given
postgres cluster, in order to implement `NetworkPolicy`.
The default is empty.
@ -196,8 +213,7 @@ configuration they are grouped under the `kubernetes` key.
* **toleration**
a dictionary that should contain `key`, `operator`, `value` and
`effect` keys. In that case, the operator defines a pod toleration
according to the values of those keys. See [kubernetes
documentation](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/)
according to the values of those keys. See [kubernetes documentation](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/)
for details on taints and tolerations. The default is empty.
* **pod_environment_configmap**
@ -208,31 +224,40 @@ configuration they are grouped under the `kubernetes` key.
operator. The default is empty.
* **pod_priority_class_name**
a name of the [priority
class](https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass)
that should be assigned to the Postgres pods. The priority class itself must be defined in advance.
Default is empty (use the default priority class).
a name of the [priority class](https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass)
that should be assigned to the Postgres pods. The priority class itself must
be defined in advance. Default is empty (use the default priority class).
* **spilo_fsgroup**
the Persistent Volumes for the spilo pods in the StatefulSet will be owned and writable by the group ID specified.
This is required to run Spilo as a non-root process, but requires a custom spilo image. Note the FSGroup of a Pod
cannot be changed without recreating a new Pod.
* **spilo_privileged**
whether the Spilo container should run in privileged mode. Privileged mode is used for AWS volume resizing and not required if you don't need that capability. The default is `false`.
whether the Spilo container should run in privileged mode. Privileged mode is
used for AWS volume resizing and not required if you don't need that
capability. The default is `false`.
* **master_pod_move_timeout**
The period of time to wait for the success of migration of master pods from an unschedulable node.
The migration includes Patroni switchovers to respective replicas on healthy nodes. The situation where master pods still exist on the old node after this timeout expires has to be fixed manually. The default is 20 minutes.
The period of time to wait for the success of migration of master pods from
an unschedulable node. The migration includes Patroni switchovers to
respective replicas on healthy nodes. The situation where master pods still
exist on the old node after this timeout expires has to be fixed manually.
The default is 20 minutes.
* **enable_pod_antiaffinity**
toggles [pod anti affinity](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/) on the Postgres pods, to avoid multiple pods
of the same Postgres cluster in the same topology , e.g. node. The default is `false`.
toggles [pod anti affinity](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/)
on the Postgres pods, to avoid multiple pods of the same Postgres cluster in
the same topology , e.g. node. The default is `false`.
* **pod_antiaffinity_topology_key**
override
[topology key](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#interlude-built-in-node-labels)
override [topology key](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#interlude-built-in-node-labels)
for pod anti affinity. The default is `kubernetes.io/hostname`.
* **pod_management_policy**
specify the
[pod management policy](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#pod-management-policies)
of stateful sets of PG clusters. The default is `ordered_ready`, the second possible value is `parallel`.
specify the [pod management policy](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#pod-management-policies)
of stateful sets of PG clusters. The default is `ordered_ready`, the second
possible value is `parallel`.
## Kubernetes resource requests
@ -257,7 +282,14 @@ CRD-based configuration.
settings. The default is `1Gi`.
* **set_memory_request_to_limit**
Set `memory_request` to `memory_limit` for all Postgres clusters (the default value is also increased). This prevents certain cases of memory overcommitment at the cost of overprovisioning memory and potential scheduling problems for containers with high memory limits due to the lack of memory on Kubernetes cluster nodes. This affects all containers created by the operator (Postgres, Scalyr sidecar, and other sidecars); to set resources for the operator's own container, change the [operator deployment manually](https://github.com/zalando/postgres-operator/blob/master/manifests/postgres-operator.yaml#L13). The default is `false`.
Set `memory_request` to `memory_limit` for all Postgres clusters (the default
value is also increased). This prevents certain cases of memory overcommitment
at the cost of overprovisioning memory and potential scheduling problems for
containers with high memory limits due to the lack of memory on Kubernetes
cluster nodes. This affects all containers created by the operator (Postgres,
Scalyr sidecar, and other sidecars); to set resources for the operator's own
container, change the [operator deployment manually](https://github.com/zalando/postgres-operator/blob/master/manifests/postgres-operator.yaml#L13).
The default is `false`.
* **enable_shm_volume**
Instruct operator to start any new database pod without limitations on shm
@ -344,9 +376,10 @@ In the CRD-based configuration they are grouped under the `load_balancer` key.
## AWS or GCP interaction
The options in this group configure operator interactions with non-Kubernetes
objects from Amazon Web Services (AWS) or Google Cloud Platform (GCP). They have no effect unless you are using
either. In the CRD-based configuration those options are grouped under the
`aws_or_gcp` key. Note the GCP integration is not yet officially supported.
objects from Amazon Web Services (AWS) or Google Cloud Platform (GCP). They have
no effect unless you are using either. In the CRD-based configuration those
options are grouped under the `aws_or_gcp` key. Note the GCP integration is not
yet officially supported.
* **wal_s3_bucket**
S3 bucket to use for shipping WAL segments with WAL-E. A bucket has to be
@ -355,7 +388,8 @@ either. In the CRD-based configuration those options are grouped under the
* **log_s3_bucket**
S3 bucket to use for shipping postgres daily logs. Works only with S3 on AWS.
The bucket has to be present and accessible by Postgres pods. The default is empty.
The bucket has to be present and accessible by Postgres pods. The default is
empty.
* **kube_iam_role**
AWS IAM role to supply in the `iam.amazonaws.com/role` annotation of Postgres
@ -376,8 +410,8 @@ Options to aid debugging of the operator itself. Grouped under the `debug` key.
* **enable_database_access**
boolean parameter that toggles the functionality of the operator that require
access to the postgres database, i.e. creating databases and users. The default
is `true`.
access to the postgres database, i.e. creating databases and users. The
default is `true`.
## Automatic creation of human users in the database
@ -414,7 +448,10 @@ key.
`admin`, that role is created by Spilo as a `NOLOGIN` role.
* **enable_admin_role_for_users**
if `true`, the `team_admin_role` will have the rights to grant roles coming from PG manifests. Such roles will be created as in "CREATE ROLE 'role_from_manifest' ... ADMIN 'team_admin_role'". The default is `true`.
if `true`, the `team_admin_role` will have the rights to grant roles coming
from PG manifests. Such roles will be created as in
"CREATE ROLE 'role_from_manifest' ... ADMIN 'team_admin_role'".
The default is `true`.
* **pam_role_name**
when set, the operator will add all team member roles to this group and add a
@ -433,11 +470,14 @@ key.
infrastructure role. The default is `admin`.
* **postgres_superuser_teams**
List of teams which members need the superuser role in each PG database cluster to administer Postgres and maintain infrastructure built around it. The default is empty.
List of teams which members need the superuser role in each PG database
cluster to administer Postgres and maintain infrastructure built around it.
The default is empty.
## Logging and REST API
Parameters affecting logging and REST API listener. In the CRD-based configuration they are grouped under the `logging_rest_api` key.
Parameters affecting logging and REST API listener. In the CRD-based
configuration they are grouped under the `logging_rest_api` key.
* **api_port**
REST API listener listens to this port. The default is `8080`.
@ -476,4 +516,22 @@ scalyr sidecar. In the CRD-based configuration they are grouped under the
Memory limit value for the Scalyr sidecar. The default is `1Gi`.
For the configmap operator configuration, the [default parameter values](https://github.com/zalando/postgres-operator/blob/master/pkg/util/config/config.go#L14) mentioned here are likely to be overwritten in your local operator installation via your local version of the operator configmap. In the case you use the operator CRD, all the CRD defaults are provided in the [operator's default configuration manifest](https://github.com/zalando/postgres-operator/blob/master/manifests/postgresql-operator-default-configuration.yaml)
## Logical backup
These parameters configure a k8s cron job managed by the operator to produce
Postgres logical backups. In the CRD-based configuration those parameters are
grouped under the `logical_backup` key.
* **logical_backup_schedule**
Backup schedule in the cron format. Please take [the reference schedule format](https://kubernetes.io/docs/tasks/job/automated-tasks-with-cron-jobs/#schedule) into account. Default: "30 00 \* \* \*"
* **logical_backup_docker_image**
An image for pods of the logical backup job. The [example image](../../docker/logical-backup/Dockerfile)
runs `pg_dumpall` on a replica if possible and uploads compressed results to
an S3 bucket under the key `/spilo/pg_cluster_name/cluster_k8s_uuid/logical_backups`.
The default image is the same image built with the Zalando-internal CI
pipeline. Default: "registry.opensource.zalan.do/acid/logical-backup"
* **logical_backup_s3_bucket**
S3 bucket to store backup results. The bucket has to be present and
accessible by Postgres pods. Default: empty.

View File

@ -287,6 +287,13 @@ spec:
sidecars:
- name: "container-name"
image: "company/image:tag"
resources:
limits:
cpu: 500m
memory: 500Mi
requests:
cpu: 100m
memory: 100Mi
env:
- name: "ENV_VAR_NAME"
value: "any-k8s-env-things"
@ -364,3 +371,11 @@ every 6 hours.
Note that if the statefulset is scaled down before resizing the size changes
are only applied to the volumes attached to the running pods. The size of the
volumes that correspond to the previously running pods is not changed.
## Logical backups
If you add
```
enableLogicalBackup: true
```
to the cluster manifest, the operator will create and sync a k8s cron job to do periodic logical backups of this particular Postgres cluster. Due to the [limitation of Kubernetes cron jobs](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#cron-job-limitations) it is highly advisable to set up additional monitoring for this feature; such monitoring is outside of the scope of operator responsibilities. See [configuration reference](reference/cluster_manifest.md) and [administrator documentation](administrator.md) for details on how backups are executed.

22
e2e/Dockerfile Normal file
View File

@ -0,0 +1,22 @@
FROM ubuntu:18.04
LABEL maintainer="Team ACID @ Zalando <team-acid@zalando.de>"
WORKDIR /e2e
COPY manifests ./manifests
COPY e2e/requirements.txt e2e/tests ./
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
python3 \
python3-setuptools \
python3-pip \
curl \
&& pip3 install --no-cache-dir -r requirements.txt \
&& curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.14.0/bin/linux/amd64/kubectl \
&& chmod +x ./kubectl \
&& mv ./kubectl /usr/local/bin/kubectl \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
CMD ["python3", "-m", "unittest", "discover", "--start-directory", ".", "-v"]

View File

@ -0,0 +1,6 @@
kind: Cluster
apiVersion: kind.sigs.k8s.io/v1alpha3
nodes:
- role: control-plane
- role: worker
- role: worker

3
e2e/requirements.txt Normal file
View File

@ -0,0 +1,3 @@
kubernetes==9.0.0
timeout_decorator==0.4.1
pyyaml==5.1

58
e2e/run.sh Executable file
View File

@ -0,0 +1,58 @@
#!/usr/bin/env bash
# enable unofficial bash strict mode
set -o errexit
set -o nounset
set -o pipefail
IFS=$'\n\t'
readonly cluster_name="postgres-operator-e2e-tests"
readonly operator_image=$(docker images --filter=reference="registry.opensource.zalan.do/acid/postgres-operator" --format "{{.Repository}}:{{.Tag}}" | head -1)
readonly e2e_test_image=${cluster_name}
readonly kubeconfig_path="/tmp/kind-config-${cluster_name}"
function start_kind(){
# avoid interference with previous test runs
if [[ $(kind-linux-amd64 get clusters | grep "^${cluster_name}*") != "" ]]
then
kind-linux-amd64 delete cluster --name ${cluster_name}
fi
kind-linux-amd64 create cluster --name ${cluster_name} --config ./e2e/kind-cluster-postgres-operator-e2e-tests.yaml
kind-linux-amd64 load docker-image "${operator_image}" --name ${cluster_name}
KUBECONFIG="$(kind-linux-amd64 get kubeconfig-path --name=${cluster_name})"
export KUBECONFIG
}
function set_kind_api_server_ip(){
# use the actual kubeconfig to connect to the 'kind' API server
# but update the IP address of the API server to the one from the Docker 'bridge' network
cp "${KUBECONFIG}" /tmp
readonly local kind_api_server_port=6443 # well-known in the 'kind' codebase
readonly local kind_api_server=$(docker inspect --format "{{ .NetworkSettings.IPAddress }}:${kind_api_server_port}" "${cluster_name}"-control-plane)
sed -i "s/server.*$/server: https:\/\/$kind_api_server/g" "${kubeconfig_path}"
}
function run_tests(){
docker run --rm --mount type=bind,source="$(readlink -f ${kubeconfig_path})",target=/root/.kube/config -e OPERATOR_IMAGE="${operator_image}" "${e2e_test_image}"
}
function clean_up(){
unset KUBECONFIG
kind-linux-amd64 delete cluster --name ${cluster_name}
rm -rf ${kubeconfig_path}
}
function main(){
trap "clean_up" QUIT TERM EXIT
start_kind
set_kind_api_server_ip
run_tests
exit 0
}
main "$@"

327
e2e/tests/test_e2e.py Normal file
View File

@ -0,0 +1,327 @@
import unittest
import time
import timeout_decorator
import subprocess
import warnings
import os
import yaml
from kubernetes import client, config
class EndToEndTestCase(unittest.TestCase):
'''
Test interaction of the operator with multiple k8s components.
'''
# `kind` pods may stuck in the `Terminating` phase for a few minutes; hence high test timeout
TEST_TIMEOUT_SEC = 600
@classmethod
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def setUpClass(cls):
'''
Deploy operator to a "kind" cluster created by /e2e/run.sh using examples from /manifests.
This operator deployment is to be shared among all tests.
/e2e/run.sh deletes the 'kind' cluster after successful run along with all operator-related entities.
In the case of test failure the cluster will stay to enable manual examination;
next invocation of "make e2e-run" will re-create it.
'''
# set a single k8s wrapper for all tests
k8s = cls.k8s = K8s()
# operator deploys pod service account there on start up
# needed for test_multi_namespace_support()
cls.namespace = "test"
v1_namespace = client.V1Namespace(metadata=client.V1ObjectMeta(name=cls.namespace))
k8s.api.core_v1.create_namespace(v1_namespace)
# submit the most recent operator image built on the Docker host
with open("manifests/postgres-operator.yaml", 'r+') as f:
operator_deployment = yaml.safe_load(f)
operator_deployment["spec"]["template"]["spec"]["containers"][0]["image"] = os.environ['OPERATOR_IMAGE']
yaml.dump(operator_deployment, f, Dumper=yaml.Dumper)
for filename in ["operator-service-account-rbac.yaml",
"configmap.yaml",
"postgres-operator.yaml"]:
k8s.create_with_kubectl("manifests/" + filename)
k8s.wait_for_operator_pod_start()
actual_operator_image = k8s.api.core_v1.list_namespaced_pod(
'default', label_selector='name=postgres-operator').items[0].spec.containers[0].image
print("Tested operator image: {}".format(actual_operator_image)) # shows up after tests finish
k8s.create_with_kubectl("manifests/minimal-postgres-manifest.yaml")
k8s.wait_for_pod_start('spilo-role=master')
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_multi_namespace_support(self):
'''
Create a customized Postgres cluster in a non-default namespace.
'''
k8s = self.k8s
with open("manifests/complete-postgres-manifest.yaml", 'r+') as f:
pg_manifest = yaml.safe_load(f)
pg_manifest["metadata"]["namespace"] = self.namespace
yaml.dump(pg_manifest, f, Dumper=yaml.Dumper)
k8s.create_with_kubectl("manifests/complete-postgres-manifest.yaml")
k8s.wait_for_pod_start("spilo-role=master", self.namespace)
self.assert_master_is_unique(self.namespace, version="acid-test-cluster")
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_scaling(self):
"""
Scale up from 2 to 3 and back to 2 pods by updating the Postgres manifest at runtime.
"""
k8s = self.k8s
labels = "version=acid-minimal-cluster"
k8s.wait_for_pg_to_scale(3)
self.assertEqual(3, k8s.count_pods_with_label(labels))
self.assert_master_is_unique()
k8s.wait_for_pg_to_scale(2)
self.assertEqual(2, k8s.count_pods_with_label(labels))
self.assert_master_is_unique()
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_taint_based_eviction(self):
"""
Add taint "postgres=:NoExecute" to node with master. This must cause a failover.
"""
k8s = self.k8s
cluster_label = 'version=acid-minimal-cluster'
# get nodes of master and replica(s) (expected target of new master)
current_master_node, failover_targets = k8s.get_pg_nodes(cluster_label)
num_replicas = len(failover_targets)
# if all pods live on the same node, failover will happen to other worker(s)
failover_targets = [x for x in failover_targets if x != current_master_node]
if len(failover_targets) == 0:
nodes = k8s.api.core_v1.list_node()
for n in nodes.items:
if "node-role.kubernetes.io/master" not in n.metadata.labels and n.metadata.name != current_master_node:
failover_targets.append(n.metadata.name)
# taint node with postgres=:NoExecute to force failover
body = {
"spec": {
"taints": [
{
"effect": "NoExecute",
"key": "postgres"
}
]
}
}
# patch node and test if master is failing over to one of the expected nodes
k8s.api.core_v1.patch_node(current_master_node, body)
k8s.wait_for_master_failover(failover_targets)
k8s.wait_for_pod_start('spilo-role=replica')
new_master_node, new_replica_nodes = k8s.get_pg_nodes(cluster_label)
self.assertNotEqual(current_master_node, new_master_node,
"Master on {} did not fail over to one of {}".format(current_master_node, failover_targets))
self.assertEqual(num_replicas, len(new_replica_nodes),
"Expected {} replicas, found {}".format(num_replicas, len(new_replica_nodes)))
self.assert_master_is_unique()
# undo the tainting
body = {
"spec": {
"taints": []
}
}
k8s.api.core_v1.patch_node(new_master_node, body)
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_logical_backup_cron_job(self):
"""
Ensure we can (a) create the cron job at user request for a specific PG cluster
(b) update the cluster-wide image for the logical backup pod
(c) delete the job at user request
Limitations:
(a) Does not run the actual batch job because there is no S3 mock to upload backups to
(b) Assumes 'acid-minimal-cluster' exists as defined in setUp
"""
k8s = self.k8s
# create the cron job
schedule = "7 7 7 7 *"
pg_patch_enable_backup = {
"spec": {
"enableLogicalBackup": True,
"logicalBackupSchedule": schedule
}
}
k8s.api.custom_objects_api.patch_namespaced_custom_object(
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_enable_backup)
k8s.wait_for_logical_backup_job_creation()
jobs = k8s.get_logical_backup_job().items
self.assertEqual(1, len(jobs), "Expected 1 logical backup job, found {}".format(len(jobs)))
job = jobs[0]
self.assertEqual(job.metadata.name, "logical-backup-acid-minimal-cluster",
"Expected job name {}, found {}"
.format("logical-backup-acid-minimal-cluster", job.metadata.name))
self.assertEqual(job.spec.schedule, schedule,
"Expected {} schedule, found {}"
.format(schedule, job.spec.schedule))
# update the cluster-wide image of the logical backup pod
image = "test-image-name"
config_map_patch = {
"data": {
"logical_backup_docker_image": image,
}
}
k8s.api.core_v1.patch_namespaced_config_map("postgres-operator", "default", config_map_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
actual_image = jobs[0].spec.job_template.spec.template.spec.containers[0].image
self.assertEqual(actual_image, image,
"Expected job image {}, found {}".format(image, actual_image))
# delete the logical backup cron job
pg_patch_disable_backup = {
"spec": {
"enableLogicalBackup": False,
}
}
k8s.api.custom_objects_api.patch_namespaced_custom_object(
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_disable_backup)
k8s.wait_for_logical_backup_job_deletion()
jobs = k8s.get_logical_backup_job().items
self.assertEqual(0, len(jobs),
"Expected 0 logical backup jobs, found {}".format(len(jobs)))
def assert_master_is_unique(self, namespace='default', version="acid-minimal-cluster"):
"""
Check that there is a single pod in the k8s cluster with the label "spilo-role=master"
To be called manually after operations that affect pods
"""
k8s = self.k8s
labels = 'spilo-role=master,version=' + version
num_of_master_pods = k8s.count_pods_with_label(labels, namespace)
self.assertEqual(num_of_master_pods, 1, "Expected 1 master pod, found {}".format(num_of_master_pods))
class K8sApi:
def __init__(self):
# https://github.com/kubernetes-client/python/issues/309
warnings.simplefilter("ignore", ResourceWarning)
self.config = config.load_kube_config()
self.k8s_client = client.ApiClient()
self.core_v1 = client.CoreV1Api()
self.apps_v1 = client.AppsV1Api()
self.batch_v1_beta1 = client.BatchV1beta1Api()
self.custom_objects_api = client.CustomObjectsApi()
class K8s:
'''
Wraps around K8 api client and helper methods.
'''
RETRY_TIMEOUT_SEC = 5
def __init__(self):
self.api = K8sApi()
def get_pg_nodes(self, pg_cluster_name, namespace='default'):
master_pod_node = ''
replica_pod_nodes = []
podsList = self.api.core_v1.list_namespaced_pod(namespace, label_selector=pg_cluster_name)
for pod in podsList.items:
if pod.metadata.labels.get('spilo-role') == 'master':
master_pod_node = pod.spec.node_name
elif pod.metadata.labels.get('spilo-role') == 'replica':
replica_pod_nodes.append(pod.spec.node_name)
return master_pod_node, replica_pod_nodes
def wait_for_operator_pod_start(self):
self. wait_for_pod_start("name=postgres-operator")
# HACK operator must register CRD / add existing PG clusters after pod start up
# for local execution ~ 10 seconds suffices
time.sleep(60)
def wait_for_pod_start(self, pod_labels, namespace='default'):
pod_phase = 'No pod running'
while pod_phase != 'Running':
pods = self.api.core_v1.list_namespaced_pod(namespace, label_selector=pod_labels).items
if pods:
pod_phase = pods[0].status.phase
time.sleep(self.RETRY_TIMEOUT_SEC)
def wait_for_pg_to_scale(self, number_of_instances, namespace='default'):
body = {
"spec": {
"numberOfInstances": number_of_instances
}
}
_ = self.api.custom_objects_api.patch_namespaced_custom_object(
"acid.zalan.do", "v1", namespace, "postgresqls", "acid-minimal-cluster", body)
labels = 'version=acid-minimal-cluster'
while self.count_pods_with_label(labels) != number_of_instances:
time.sleep(self.RETRY_TIMEOUT_SEC)
def count_pods_with_label(self, labels, namespace='default'):
return len(self.api.core_v1.list_namespaced_pod(namespace, label_selector=labels).items)
def wait_for_master_failover(self, expected_master_nodes, namespace='default'):
pod_phase = 'Failing over'
new_master_node = ''
labels = 'spilo-role=master,version=acid-minimal-cluster'
while (pod_phase != 'Running') or (new_master_node not in expected_master_nodes):
pods = self.api.core_v1.list_namespaced_pod(namespace, label_selector=labels).items
if pods:
new_master_node = pods[0].spec.node_name
pod_phase = pods[0].status.phase
time.sleep(self.RETRY_TIMEOUT_SEC)
def get_logical_backup_job(self, namespace='default'):
return self.api.batch_v1_beta1.list_namespaced_cron_job(namespace, label_selector="application=spilo")
def wait_for_logical_backup_job(self, expected_num_of_jobs):
while (len(self.get_logical_backup_job().items) != expected_num_of_jobs):
time.sleep(self.RETRY_TIMEOUT_SEC)
def wait_for_logical_backup_job_deletion(self):
self.wait_for_logical_backup_job(expected_num_of_jobs=0)
def wait_for_logical_backup_job_creation(self):
self.wait_for_logical_backup_job(expected_num_of_jobs=1)
def create_with_kubectl(self, path):
subprocess.run(["kubectl", "create", "-f", path])
if __name__ == '__main__':
unittest.main()

View File

@ -47,13 +47,13 @@ spec:
pg_hba:
- hostssl all all 0.0.0.0/0 md5
- host all all 0.0.0.0/0 md5
slots:
permanent_physical_1:
type: physical
permanent_logical_1:
type: logical
database: foo
plugin: pgoutput
#slots:
# permanent_physical_1:
# type: physical
# permanent_logical_1:
# type: logical
# database: foo
# plugin: pgoutput
ttl: 30
loop_wait: &loop_wait 10
retry_timeout: 10

View File

@ -2,7 +2,7 @@ apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-operator
data:
data:
watched_namespace: "*" # listen to all namespaces
cluster_labels: application:spilo
cluster_name_label: version
@ -10,9 +10,10 @@ data:
debug_logging: "true"
workers: "4"
docker_image: registry.opensource.zalan.do/acid/spilo-cdp-11:1.5-p70
docker_image: registry.opensource.zalan.do/acid/spilo-11:1.5-p7
pod_service_account_name: "zalando-postgres-operator"
secret_name_template: '{username}.{cluster}.credentials'
cluster_domain: cluster.local
super_username: postgres
enable_teams_api: "false"
spilo_privileged: "false"
@ -54,3 +55,7 @@ data:
resource_check_interval: 3s
resource_check_timeout: 10m
resync_period: 5m
# logical_backup_schedule: "30 00 * * *"
# logical_backup_docker_image: "registry.opensource.zalan.do/acid/logical-backup"
# logical_backup_s3_bucket: ""

View File

@ -17,7 +17,6 @@ spec:
# role for application foo
foo_user: []
#databases: name->owner
databases:
foo: zalando

View File

@ -26,6 +26,7 @@ rules:
- create
- get
- patch
- update
- apiGroups:
- ""
resources:
@ -141,7 +142,17 @@ rules:
- bind
resourceNames:
- zalando-postgres-operator
- apiGroups:
- batch
resources:
- cronjobs # enables logical backups
verbs:
- create
- delete
- get
- list
- patch
- update
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding

View File

@ -4,7 +4,7 @@ metadata:
name: postgresql-operator-default-configuration
configuration:
etcd_host: ""
docker_image: registry.opensource.zalan.do/acid/spilo-cdp-11:1.5-p42
docker_image: registry.opensource.zalan.do/acid/spilo-11:1.5-p7
workers: 4
min_instances: -1
max_instances: -1
@ -21,8 +21,10 @@ configuration:
pod_terminate_grace_period: 5m
pdb_name_format: "postgres-{cluster}-pdb"
secret_name_template: "{username}.{cluster}.credentials.{tprkind}.{tprgroup}"
cluster_domain: cluster.local
oauth_token_secret_name: postgresql-operator
pod_role_label: spilo-role
# spilo_fsgroup: 103
spilo_privileged: false
cluster_labels:
application: spilo
@ -36,7 +38,7 @@ configuration:
# infrastructure_roles_secret_name: ""
# pod_environment_configmap: ""
pod_management_policy: "ordered_ready"
enable_pod_antiaffinity: "false"
enable_pod_antiaffinity: false
pod_antiaffinity_topology_key: "kubernetes.io/hostname"
postgres_pod_resources:
default_cpu_request: 100m
@ -91,4 +93,7 @@ configuration:
# scalyr_api_key: ""
# scalyr_image: ""
# scalyr_server_url: ""
logical_backup:
logical_backup_schedule: "30 00 * * *"
logical_backup_docker_image: "registry.opensource.zalan.do/acid/logical-backup"
logical_backup_s3_bucket: ""

View File

@ -46,9 +46,11 @@ type KubernetesMetaConfiguration struct {
PodServiceAccountRoleBindingDefinition string `json:"pod_service_account_role_binding_definition,omitempty"`
PodTerminateGracePeriod Duration `json:"pod_terminate_grace_period,omitempty"`
SpiloPrivileged bool `json:"spilo_privileged,omitemty"`
SpiloFSGroup *int64 `json:"spilo_fsgroup,omitempty"`
WatchedNamespace string `json:"watched_namespace,omitempty"`
PDBNameFormat config.StringTemplate `json:"pdb_name_format,omitempty"`
SecretNameTemplate config.StringTemplate `json:"secret_name_template,omitempty"`
ClusterDomain string `json:"cluster_domain"`
OAuthTokenSecretName spec.NamespacedName `json:"oauth_token_secret_name,omitempty"`
InfrastructureRolesSecretName spec.NamespacedName `json:"infrastructure_roles_secret_name,omitempty"`
PodRoleLabel string `json:"pod_role_label,omitempty"`
@ -143,25 +145,26 @@ type ScalyrConfiguration struct {
// OperatorConfigurationData defines the operation config
type OperatorConfigurationData struct {
EtcdHost string `json:"etcd_host,omitempty"`
DockerImage string `json:"docker_image,omitempty"`
Workers uint32 `json:"workers,omitempty"`
MinInstances int32 `json:"min_instances,omitempty"`
MaxInstances int32 `json:"max_instances,omitempty"`
ResyncPeriod Duration `json:"resync_period,omitempty"`
RepairPeriod Duration `json:"repair_period,omitempty"`
Sidecars map[string]string `json:"sidecar_docker_images,omitempty"`
PostgresUsersConfiguration PostgresUsersConfiguration `json:"users"`
Kubernetes KubernetesMetaConfiguration `json:"kubernetes"`
PostgresPodResources PostgresPodResourcesDefaults `json:"postgres_pod_resources"`
SetMemoryRequestToLimit bool `json:"set_memory_request_to_limit,omitempty"`
Timeouts OperatorTimeouts `json:"timeouts"`
LoadBalancer LoadBalancerConfiguration `json:"load_balancer"`
AWSGCP AWSGCPConfiguration `json:"aws_or_gcp"`
OperatorDebug OperatorDebugConfiguration `json:"debug"`
TeamsAPI TeamsAPIConfiguration `json:"teams_api"`
LoggingRESTAPI LoggingRESTAPIConfiguration `json:"logging_rest_api"`
Scalyr ScalyrConfiguration `json:"scalyr"`
EtcdHost string `json:"etcd_host,omitempty"`
DockerImage string `json:"docker_image,omitempty"`
Workers uint32 `json:"workers,omitempty"`
MinInstances int32 `json:"min_instances,omitempty"`
MaxInstances int32 `json:"max_instances,omitempty"`
ResyncPeriod Duration `json:"resync_period,omitempty"`
RepairPeriod Duration `json:"repair_period,omitempty"`
Sidecars map[string]string `json:"sidecar_docker_images,omitempty"`
PostgresUsersConfiguration PostgresUsersConfiguration `json:"users"`
Kubernetes KubernetesMetaConfiguration `json:"kubernetes"`
PostgresPodResources PostgresPodResourcesDefaults `json:"postgres_pod_resources"`
SetMemoryRequestToLimit bool `json:"set_memory_request_to_limit,omitempty"`
Timeouts OperatorTimeouts `json:"timeouts"`
LoadBalancer LoadBalancerConfiguration `json:"load_balancer"`
AWSGCP AWSGCPConfiguration `json:"aws_or_gcp"`
OperatorDebug OperatorDebugConfiguration `json:"debug"`
TeamsAPI TeamsAPIConfiguration `json:"teams_api"`
LoggingRESTAPI LoggingRESTAPIConfiguration `json:"logging_rest_api"`
Scalyr ScalyrConfiguration `json:"scalyr"`
LogicalBackup OperatorLogicalBackupConfiguration `json:"logical_backup"`
}
// OperatorConfigurationUsers defines configration for super user
@ -174,3 +177,9 @@ type OperatorConfigurationUsers struct {
//Duration shortens this frequently used name
type Duration time.Duration
type OperatorLogicalBackupConfiguration struct {
Schedule string `json:"logical_backup_schedule,omitempty"`
DockerImage string `json:"logical_backup_docker_image,omitempty"`
S3Bucket string `json:"logical_backup_s3_bucket,omitempty"`
}

View File

@ -66,6 +66,11 @@ func (in *CloneDescription) DeepCopy() *CloneDescription {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesMetaConfiguration) DeepCopyInto(out *KubernetesMetaConfiguration) {
*out = *in
if in.SpiloFSGroup != nil {
in, out := &in.SpiloFSGroup, &out.SpiloFSGroup
*out = new(int64)
**out = **in
}
out.OAuthTokenSecretName = in.OAuthTokenSecretName
out.InfrastructureRolesSecretName = in.InfrastructureRolesSecretName
if in.ClusterLabels != nil {
@ -211,6 +216,7 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData
in.TeamsAPI.DeepCopyInto(&out.TeamsAPI)
out.LoggingRESTAPI = in.LoggingRESTAPI
out.Scalyr = in.Scalyr
out.LogicalBackup = in.LogicalBackup
return
}
@ -301,6 +307,22 @@ func (in *OperatorDebugConfiguration) DeepCopy() *OperatorDebugConfiguration {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OperatorLogicalBackupConfiguration) DeepCopyInto(out *OperatorLogicalBackupConfiguration) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorLogicalBackupConfiguration.
func (in *OperatorLogicalBackupConfiguration) DeepCopy() *OperatorLogicalBackupConfiguration {
if in == nil {
return nil
}
out := new(OperatorLogicalBackupConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OperatorTimeouts) DeepCopyInto(out *OperatorTimeouts) {
*out = *in

View File

@ -81,6 +81,7 @@ type Cluster struct {
currentProcess Process
processMu sync.RWMutex // protects the current operation for reporting, no need to hold the master mutex
specMu sync.RWMutex // protects the spec for reporting, no need to hold the master mutex
}
type compareStatefulsetResult struct {
@ -298,6 +299,13 @@ func (c *Cluster) Create() error {
c.logger.Infof("databases have been successfully created")
}
if c.Postgresql.Spec.EnableLogicalBackup {
if err := c.createLogicalBackupJob(); err != nil {
return fmt.Errorf("could not create a k8s cron job for logical backups: %v", err)
}
c.logger.Info("a k8s cron job for logical backup has been successfully created")
}
if err := c.listResources(); err != nil {
c.logger.Errorf("could not list resources: %v", err)
}
@ -334,7 +342,7 @@ func (c *Cluster) compareStatefulSetWith(statefulSet *v1beta1.StatefulSet) *comp
if c.Statefulset.Spec.Template.Spec.ServiceAccountName != statefulSet.Spec.Template.Spec.ServiceAccountName {
needsReplace = true
needsRollUpdate = true
reasons = append(reasons, "new statefulset's serviceAccountName service asccount name doesn't match the current one")
reasons = append(reasons, "new statefulset's serviceAccountName service account name doesn't match the current one")
}
if *c.Statefulset.Spec.Template.Spec.TerminationGracePeriodSeconds != *statefulSet.Spec.Template.Spec.TerminationGracePeriodSeconds {
needsReplace = true
@ -454,16 +462,16 @@ func (c *Cluster) compareContainers(description string, setA, setB []v1.Containe
func compareResources(a *v1.ResourceRequirements, b *v1.ResourceRequirements) bool {
equal := true
if a != nil {
equal = compareResoucesAssumeFirstNotNil(a, b)
equal = compareResourcesAssumeFirstNotNil(a, b)
}
if equal && (b != nil) {
equal = compareResoucesAssumeFirstNotNil(b, a)
equal = compareResourcesAssumeFirstNotNil(b, a)
}
return equal
}
func compareResoucesAssumeFirstNotNil(a *v1.ResourceRequirements, b *v1.ResourceRequirements) bool {
func compareResourcesAssumeFirstNotNil(a *v1.ResourceRequirements, b *v1.ResourceRequirements) bool {
if b == nil || (len(b.Requests) == 0) {
return len(a.Requests) == 0
}
@ -481,8 +489,10 @@ func compareResoucesAssumeFirstNotNil(a *v1.ResourceRequirements, b *v1.Resource
}
// Update changes Kubernetes objects according to the new specification. Unlike the sync case, the missing object.
// (i.e. service) is treated as an error.
// 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
// for a cluster that had no such job before. In this case a missing job is not an error.
func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
updateFailed := false
@ -569,6 +579,43 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
}
}()
// logical backup job
func() {
// create if it did not exist
if !oldSpec.Spec.EnableLogicalBackup && newSpec.Spec.EnableLogicalBackup {
c.logger.Debugf("creating backup cron job")
if err := c.createLogicalBackupJob(); err != nil {
c.logger.Errorf("could not create a k8s cron job for logical backups: %v", err)
updateFailed = true
return
}
}
// delete if no longer needed
if oldSpec.Spec.EnableLogicalBackup && !newSpec.Spec.EnableLogicalBackup {
c.logger.Debugf("deleting backup cron job")
if err := c.deleteLogicalBackupJob(); err != nil {
c.logger.Errorf("could not delete a k8s cron job for logical backups: %v", err)
updateFailed = true
return
}
}
// apply schedule changes
// this is the only parameter of logical backups a user can overwrite in the cluster manifest
if (oldSpec.Spec.EnableLogicalBackup && newSpec.Spec.EnableLogicalBackup) &&
(newSpec.Spec.LogicalBackupSchedule != oldSpec.Spec.LogicalBackupSchedule) {
c.logger.Debugf("updating schedule of the backup cron job")
if err := c.syncLogicalBackupJob(); err != nil {
c.logger.Errorf("could not sync logical backup jobs: %v", err)
updateFailed = true
}
}
}()
// Roles and Databases
if !(c.databaseAccessDisabled() || c.getNumberOfInstances(&c.Spec) <= 0) {
c.logger.Debugf("syncing roles")
@ -597,6 +644,12 @@ func (c *Cluster) Delete() {
c.mu.Lock()
defer c.mu.Unlock()
// delete the backup job before the stateful set of the cluster to prevent connections to non-existing pods
// deleting the cron job also removes pods and batch jobs it created
if err := c.deleteLogicalBackupJob(); err != nil {
c.logger.Warningf("could not remove the logical backup k8s cron job; %v", err)
}
if err := c.deleteStatefulSet(); err != nil {
c.logger.Warningf("could not delete statefulset: %v", err)
}
@ -629,6 +682,7 @@ func (c *Cluster) Delete() {
if err := c.deletePatroniClusterObjects(); err != nil {
c.logger.Warningf("could not remove leftover patroni objects; %v", err)
}
}
//NeedsRepair returns true if the cluster should be included in the repair scan (based on its in-memory status).
@ -821,7 +875,7 @@ func (c *Cluster) initInfrastructureRoles() error {
return nil
}
// resolves naming conflicts between existing and new roles by chosing either of them.
// resolves naming conflicts between existing and new roles by choosing either of them.
func (c *Cluster) resolveNameConflict(currentRole, newRole *spec.PgUser) spec.PgUser {
var result spec.PgUser
if newRole.Origin >= currentRole.Origin {
@ -915,7 +969,7 @@ func (c *Cluster) Switchover(curMaster *v1.Pod, candidate spec.NamespacedName) e
// signal the role label waiting goroutine to close the shop and go home
close(stopCh)
// wait until the goroutine terminates, since unregisterPodSubscriber
// must be called before the outer return; otherwsise we risk subscribing to the same pod twice.
// must be called before the outer return; otherwise we risk subscribing to the same pod twice.
wg.Wait()
// close the label waiting channel no sooner than the waiting goroutine terminates.
close(podLabelErr)
@ -950,8 +1004,8 @@ func (c *Cluster) deletePatroniClusterObjects() error {
if !c.patroniUsesKubernetes() {
c.logger.Infof("not cleaning up Etcd Patroni objects on cluster delete")
}
c.logger.Debugf("removing leftover Patroni objects (endpoints or configmaps)")
for _, deleter := range []simpleActionWithResult{c.deletePatroniClusterEndpoints, c.deletePatroniClusterConfigMaps} {
c.logger.Debugf("removing leftover Patroni objects (endpoints, services and configmaps)")
for _, deleter := range []simpleActionWithResult{c.deletePatroniClusterEndpoints, c.deletePatroniClusterServices, c.deletePatroniClusterConfigMaps} {
if err := deleter(); err != nil {
return err
}
@ -983,6 +1037,19 @@ func (c *Cluster) deleteClusterObject(
return nil
}
func (c *Cluster) deletePatroniClusterServices() error {
get := func(name string) (spec.NamespacedName, error) {
svc, err := c.KubeClient.Services(c.Namespace).Get(name, metav1.GetOptions{})
return util.NameFromMeta(svc.ObjectMeta), err
}
deleteServiceFn := func(name string) error {
return c.KubeClient.Services(c.Namespace).Delete(name, c.deleteOptions)
}
return c.deleteClusterObject(get, deleteServiceFn, "service")
}
func (c *Cluster) deletePatroniClusterEndpoints() error {
get := func(name string) (spec.NamespacedName, error) {
ep, err := c.KubeClient.Endpoints(c.Namespace).Get(name, metav1.GetOptions{})

View File

@ -34,7 +34,7 @@ func (c *Cluster) pgConnectionString() string {
password := c.systemUsers[constants.SuperuserKeyName].Password
return fmt.Sprintf("host='%s' dbname=postgres sslmode=require user='%s' password='%s' connect_timeout='%d'",
fmt.Sprintf("%s.%s.svc.cluster.local", c.Name, c.Namespace),
fmt.Sprintf("%s.%s.svc.%s", c.Name, c.Namespace, c.OpConfig.ClusterDomain),
c.systemUsers[constants.SuperuserKeyName].Name,
strings.Replace(password, "$", "\\$", -1),
constants.PostgresConnectTimeout/time.Second)

View File

@ -20,6 +20,8 @@ import (
"github.com/zalando/postgres-operator/pkg/util"
"github.com/zalando/postgres-operator/pkg/util/config"
"github.com/zalando/postgres-operator/pkg/util/constants"
batchv1 "k8s.io/api/batch/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
"k8s.io/apimachinery/pkg/labels"
)
@ -147,7 +149,7 @@ func fillResourceList(spec acidv1.ResourceDescription, defaults acidv1.ResourceD
return requests, nil
}
func generateSpiloJSONConfiguration(pg *acidv1.PostgresqlParam, patroni *acidv1.Patroni, pamRoleName string, logger *logrus.Entry) string {
func generateSpiloJSONConfiguration(pg *acidv1.PostgresqlParam, patroni *acidv1.Patroni, pamRoleName string, logger *logrus.Entry) (string, error) {
config := spiloConfiguration{}
config.Bootstrap = pgBootstrap{}
@ -247,12 +249,9 @@ PatroniInitDBParams:
Options: []string{constants.RoleFlagCreateDB, constants.RoleFlagNoLogin},
},
}
result, err := json.Marshal(config)
if err != nil {
logger.Errorf("cannot convert spilo configuration into JSON: %v", err)
return ""
}
return string(result)
res, err := json.Marshal(config)
return string(res), err
}
func getLocalAndBoostrapPostgreSQLParameters(parameters map[string]string) (local, bootstrap map[string]string) {
@ -330,7 +329,7 @@ func tolerations(tolerationsSpec *[]v1.Toleration, podToleration map[string]stri
return []v1.Toleration{}
}
// isBootstrapOnlyParameter checks asgainst special Patroni bootstrap parameters.
// isBootstrapOnlyParameter checks against special Patroni bootstrap parameters.
// Those parameters must go to the bootstrap/dcs/postgresql/parameters section.
// See http://patroni.readthedocs.io/en/latest/dynamic_configuration.html.
func isBootstrapOnlyParameter(param string) bool {
@ -352,7 +351,7 @@ func generateVolumeMounts() []v1.VolumeMount {
}
}
func generateSpiloContainer(
func generateContainer(
name string,
dockerImage *string,
resourceRequirements *v1.ResourceRequirements,
@ -433,6 +432,7 @@ func generatePodTemplate(
initContainers []v1.Container,
sidecarContainers []v1.Container,
tolerationsSpec *[]v1.Toleration,
spiloFSGroup *int64,
nodeAffinity *v1.Affinity,
terminateGracePeriod int64,
podServiceAccountName string,
@ -446,6 +446,11 @@ func generatePodTemplate(
terminateGracePeriodSeconds := terminateGracePeriod
containers := []v1.Container{*spiloContainer}
containers = append(containers, sidecarContainers...)
securityContext := v1.PodSecurityContext{}
if spiloFSGroup != nil {
securityContext.FSGroup = spiloFSGroup
}
podSpec := v1.PodSpec{
ServiceAccountName: podServiceAccountName,
@ -453,6 +458,7 @@ func generatePodTemplate(
Containers: containers,
InitContainers: initContainers,
Tolerations: *tolerationsSpec,
SecurityContext: &securityContext,
}
if shmVolume {
@ -785,7 +791,10 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*v1beta1.State
return nil, fmt.Errorf("s3_wal_path is empty for standby cluster")
}
spiloConfiguration := generateSpiloJSONConfiguration(&spec.PostgresqlParam, &spec.Patroni, c.OpConfig.PamRoleName, c.logger)
spiloConfiguration, err := generateSpiloJSONConfiguration(&spec.PostgresqlParam, &spec.Patroni, c.OpConfig.PamRoleName, c.logger)
if err != nil {
return nil, fmt.Errorf("could not generate Spilo JSON configuration: %v", err)
}
// generate environment variables for the spilo container
spiloEnvVars := deduplicateEnvVars(
@ -799,7 +808,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*v1beta1.State
// generate the spilo container
c.logger.Debugf("Generating Spilo container, environment variables: %v", spiloEnvVars)
spiloContainer := generateSpiloContainer(c.containerName(),
spiloContainer := generateContainer(c.containerName(),
&effectiveDockerImage,
resourceRequirements,
spiloEnvVars,
@ -836,6 +845,12 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*v1beta1.State
tolerationSpec := tolerations(&spec.Tolerations, c.OpConfig.PodToleration)
effectivePodPriorityClassName := util.Coalesce(spec.PodPriorityClassName, c.OpConfig.PodPriorityClassName)
// determine the FSGroup for the spilo pod
effectiveFSGroup := c.OpConfig.Resources.SpiloFSGroup
if spec.SpiloFSGroup != nil {
effectiveFSGroup = spec.SpiloFSGroup
}
// generate pod template for the statefulset, based on the spilo container and sidecars
if podTemplate, err = generatePodTemplate(
c.Namespace,
@ -844,6 +859,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*v1beta1.State
spec.InitContainers,
sidecarContainers,
&tolerationSpec,
effectiveFSGroup,
nodeAffinity(c.OpConfig.NodeReadinessLabel),
int64(c.OpConfig.PodTerminateGracePeriod.Seconds()),
c.OpConfig.PodServiceAccountName,
@ -1021,6 +1037,7 @@ func generatePersistentVolumeClaimTemplate(volumeSize, volumeStorageClass string
return nil, fmt.Errorf("could not parse volume size: %v", err)
}
volumeMode := v1.PersistentVolumeFilesystem
volumeClaim := &v1.PersistentVolumeClaim{
ObjectMeta: metadata,
Spec: v1.PersistentVolumeClaimSpec{
@ -1031,6 +1048,7 @@ func generatePersistentVolumeClaimTemplate(volumeSize, volumeStorageClass string
},
},
StorageClassName: storageClassName,
VolumeMode: &volumeMode,
},
}
@ -1310,3 +1328,168 @@ func (c *Cluster) getClusterServiceConnectionParameters(clusterName string) (hos
port = "5432"
return
}
func (c *Cluster) generateLogicalBackupJob() (*batchv1beta1.CronJob, error) {
var (
err error
podTemplate *v1.PodTemplateSpec
resourceRequirements *v1.ResourceRequirements
)
// NB: a cron job creates standard batch jobs according to schedule; these batch jobs manage pods and clean-up
c.logger.Debug("Generating logical backup pod template")
// allocate for the backup pod the same amount of resources as for normal DB pods
defaultResources := c.makeDefaultResources()
resourceRequirements, err = generateResourceRequirements(c.Spec.Resources, defaultResources)
if err != nil {
return nil, fmt.Errorf("could not generate resource requirements for logical backup pods: %v", err)
}
envVars := c.generateLogicalBackupPodEnvVars()
logicalBackupContainer := generateContainer(
"logical-backup",
&c.OpConfig.LogicalBackup.LogicalBackupDockerImage,
resourceRequirements,
envVars,
[]v1.VolumeMount{},
c.OpConfig.SpiloPrivileged, // use same value as for normal DB pods
)
labels := map[string]string{
"version": c.Name,
"application": "spilo-logical-backup",
}
podAffinityTerm := v1.PodAffinityTerm{
LabelSelector: &metav1.LabelSelector{
MatchLabels: labels,
},
TopologyKey: "kubernetes.io/hostname",
}
podAffinity := v1.Affinity{
PodAffinity: &v1.PodAffinity{
PreferredDuringSchedulingIgnoredDuringExecution: []v1.WeightedPodAffinityTerm{{
Weight: 1,
PodAffinityTerm: podAffinityTerm,
},
},
}}
// re-use the method that generates DB pod templates
if podTemplate, err = generatePodTemplate(
c.Namespace,
c.labelsSet(true),
logicalBackupContainer,
[]v1.Container{},
[]v1.Container{},
&[]v1.Toleration{},
nil,
nodeAffinity(c.OpConfig.NodeReadinessLabel),
int64(c.OpConfig.PodTerminateGracePeriod.Seconds()),
c.OpConfig.PodServiceAccountName,
c.OpConfig.KubeIAMRole,
"",
false,
false,
""); err != nil {
return nil, fmt.Errorf("could not generate pod template for logical backup pod: %v", err)
}
// overwrite specific params of logical backups pods
podTemplate.Spec.Affinity = &podAffinity
podTemplate.Spec.RestartPolicy = "Never" // affects containers within a pod
// configure a batch job
jobSpec := batchv1.JobSpec{
Template: *podTemplate,
}
// configure a cron job
jobTemplateSpec := batchv1beta1.JobTemplateSpec{
Spec: jobSpec,
}
schedule := c.Postgresql.Spec.LogicalBackupSchedule
if schedule == "" {
schedule = c.OpConfig.LogicalBackupSchedule
}
cronJob := &batchv1beta1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: c.getLogicalBackupJobName(),
Namespace: c.Namespace,
Labels: c.labelsSet(true),
},
Spec: batchv1beta1.CronJobSpec{
Schedule: schedule,
JobTemplate: jobTemplateSpec,
ConcurrencyPolicy: batchv1beta1.ForbidConcurrent,
},
}
return cronJob, nil
}
func (c *Cluster) generateLogicalBackupPodEnvVars() []v1.EnvVar {
envVars := []v1.EnvVar{
{
Name: "SCOPE",
Value: c.Name,
},
// Bucket env vars
{
Name: "LOGICAL_BACKUP_S3_BUCKET",
Value: c.OpConfig.LogicalBackup.LogicalBackupS3Bucket,
},
{
Name: "LOGICAL_BACKUP_S3_BUCKET_SCOPE_SUFFIX",
Value: getBucketScopeSuffix(string(c.Postgresql.GetUID())),
},
// Postgres env vars
{
Name: "PG_VERSION",
Value: c.Spec.PgVersion,
},
{
Name: "PGPORT",
Value: "5432",
},
{
Name: "PGUSER",
Value: c.OpConfig.SuperUsername,
},
{
Name: "PGDATABASE",
Value: c.OpConfig.SuperUsername,
},
{
Name: "PGSSLMODE",
Value: "require",
},
{
Name: "PGPASSWORD",
ValueFrom: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: c.credentialSecretName(c.OpConfig.SuperUsername),
},
Key: "password",
},
},
},
}
c.logger.Debugf("Generated logical backup env vars %v", envVars)
return envVars
}
// getLogicalBackupJobName returns the name; the job itself may not exists
func (c *Cluster) getLogicalBackupJobName() (jobName string) {
return "logical-backup-" + c.clusterName().Name
}

View File

@ -3,11 +3,12 @@ package cluster
import (
"k8s.io/api/core/v1"
"testing"
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
"github.com/zalando/postgres-operator/pkg/util/config"
"github.com/zalando/postgres-operator/pkg/util/constants"
"github.com/zalando/postgres-operator/pkg/util/k8sutil"
"testing"
)
func True() *bool {
@ -20,6 +21,69 @@ func False() *bool {
return &b
}
func TestGenerateSpiloJSONConfiguration(t *testing.T) {
var cluster = New(
Config{
OpConfig: config.Config{
ProtectedRoles: []string{"admin"},
Auth: config.Auth{
SuperUsername: superUserName,
ReplicationUsername: replicationUserName,
},
},
}, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger)
testName := "TestGenerateSpiloConfig"
tests := []struct {
subtest string
pgParam *acidv1.PostgresqlParam
patroni *acidv1.Patroni
role string
opConfig config.Config
result string
}{
{
subtest: "Patroni default configuration",
pgParam: &acidv1.PostgresqlParam{PgVersion: "9.6"},
patroni: &acidv1.Patroni{},
role: "zalandos",
opConfig: config.Config{},
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/9.6/bin"},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"}],"users":{"zalandos":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{}}}`,
},
{
subtest: "Patroni configured",
pgParam: &acidv1.PostgresqlParam{PgVersion: "11"},
patroni: &acidv1.Patroni{
InitDB: map[string]string{
"encoding": "UTF8",
"locale": "en_US.UTF-8",
"data-checksums": "true",
},
PgHba: []string{"hostssl all all 0.0.0.0/0 md5", "host all all 0.0.0.0/0 md5"},
TTL: 30,
LoopWait: 10,
RetryTimeout: 10,
MaximumLagOnFailover: 33554432,
Slots: map[string]map[string]string{"permanent_logical_1": {"type": "logical", "database": "foo", "plugin": "pgoutput"}},
},
role: "zalandos",
opConfig: config.Config{},
result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/11/bin","pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"users":{"zalandos":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"slots":{"permanent_logical_1":{"database":"foo","plugin":"pgoutput","type":"logical"}}}}}`,
},
}
for _, tt := range tests {
cluster.OpConfig = tt.opConfig
result, err := generateSpiloJSONConfiguration(tt.pgParam, tt.patroni, tt.role, logger)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if tt.result != result {
t.Errorf("%s %s: Spilo Config is %v, expected %v for role %#v and param %#v",
testName, tt.subtest, result, tt.result, tt.role, tt.pgParam)
}
}
}
func TestCreateLoadBalancerLogic(t *testing.T) {
var cluster = New(
Config{

View File

@ -6,7 +6,8 @@ import (
"strings"
"k8s.io/api/apps/v1beta1"
"k8s.io/api/core/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
v1 "k8s.io/api/core/v1"
policybeta1 "k8s.io/api/policy/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
@ -360,7 +361,7 @@ func (c *Cluster) updateService(role PostgresRole, newService *v1.Service) error
// TODO: check if it possible to change the service type with a patch in future versions of Kubernetes
if newService.Spec.Type != c.Services[role].Spec.Type {
// service type has changed, need to replace the service completely.
// we cannot use just pach the current service, since it may contain attributes incompatible with the new type.
// we cannot use just patch the current service, since it may contain attributes incompatible with the new type.
var (
currentEndpoint *v1.Endpoints
err error
@ -368,7 +369,7 @@ func (c *Cluster) updateService(role PostgresRole, newService *v1.Service) error
if role == Master {
// for the master service we need to re-create the endpoint as well. Get the up-to-date version of
// the addresses stored in it before the service is deleted (deletion of the service removes the endpooint)
// the addresses stored in it before the service is deleted (deletion of the service removes the endpoint)
currentEndpoint, err = c.KubeClient.Endpoints(c.Namespace).Get(c.endpointName(role), metav1.GetOptions{})
if err != nil {
return fmt.Errorf("could not get current cluster %s endpoints: %v", role, err)
@ -609,6 +610,51 @@ func (c *Cluster) createRoles() (err error) {
return c.syncRoles()
}
func (c *Cluster) createLogicalBackupJob() (err error) {
c.setProcessName("creating a k8s cron job for logical backups")
logicalBackupJobSpec, err := c.generateLogicalBackupJob()
if err != nil {
return fmt.Errorf("could not generate k8s cron job spec: %v", err)
}
c.logger.Debugf("Generated cronJobSpec: %v", logicalBackupJobSpec)
_, err = c.KubeClient.CronJobsGetter.CronJobs(c.Namespace).Create(logicalBackupJobSpec)
if err != nil {
return fmt.Errorf("could not create k8s cron job: %v", err)
}
return nil
}
func (c *Cluster) patchLogicalBackupJob(newJob *batchv1beta1.CronJob) error {
c.setProcessName("patching logical backup job")
patchData, err := specPatch(newJob.Spec)
if err != nil {
return fmt.Errorf("could not form patch for the logical backup job: %v", err)
}
// update the backup job spec
_, err = c.KubeClient.CronJobsGetter.CronJobs(c.Namespace).Patch(
c.getLogicalBackupJobName(),
types.MergePatchType,
patchData, "")
if err != nil {
return fmt.Errorf("could not patch logical backup job: %v", err)
}
return nil
}
func (c *Cluster) deleteLogicalBackupJob() error {
c.logger.Info("removing the logical backup job")
return c.KubeClient.CronJobsGetter.CronJobs(c.Namespace).Delete(c.getLogicalBackupJobName(), c.deleteOptions)
}
// GetServiceMaster returns cluster's kubernetes master Service
func (c *Cluster) GetServiceMaster() *v1.Service {
return c.Services[Master]

View File

@ -3,7 +3,8 @@ package cluster
import (
"fmt"
"k8s.io/api/core/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
v1 "k8s.io/api/core/v1"
policybeta1 "k8s.io/api/policy/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -92,6 +93,16 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error {
return err
}
// create a logical backup job unless we are running without pods or disable that feature explicitly
if c.Spec.EnableLogicalBackup && c.getNumberOfInstances(&c.Spec) > 0 {
c.logger.Debug("syncing logical backup job")
if err = c.syncLogicalBackupJob(); err != nil {
err = fmt.Errorf("could not sync the logical backup job: %v", err)
return err
}
}
return err
}
@ -519,3 +530,56 @@ func (c *Cluster) syncDatabases() error {
return nil
}
func (c *Cluster) syncLogicalBackupJob() error {
var (
job *batchv1beta1.CronJob
desiredJob *batchv1beta1.CronJob
err error
)
c.setProcessName("syncing the logical backup job")
// sync the job if it exists
jobName := c.getLogicalBackupJobName()
if job, err = c.KubeClient.CronJobsGetter.CronJobs(c.Namespace).Get(jobName, metav1.GetOptions{}); err == nil {
desiredJob, err = c.generateLogicalBackupJob()
if err != nil {
return fmt.Errorf("could not generate the desired logical backup job state: %v", err)
}
if match, reason := k8sutil.SameLogicalBackupJob(job, desiredJob); !match {
c.logger.Infof("logical job %q is not in the desired state and needs to be updated",
c.getLogicalBackupJobName(),
)
if reason != "" {
c.logger.Infof("reason: %s", reason)
}
if err = c.patchLogicalBackupJob(desiredJob); err != nil {
return fmt.Errorf("could not update logical backup job to match desired state: %v", err)
}
c.logger.Info("the logical backup job is synced")
}
return nil
}
if !k8sutil.ResourceNotFound(err) {
return fmt.Errorf("could not get logical backp job: %v", err)
}
// no existing logical backup job, create new one
c.logger.Info("could not find the cluster's logical backup job")
if err = c.createLogicalBackupJob(); err == nil {
c.logger.Infof("created missing logical backup job %q", jobName)
} else {
if !k8sutil.ResourceAlreadyExists(err) {
return fmt.Errorf("could not create missing logical backup job: %v", err)
}
c.logger.Infof("logical backup job %q already exists", jobName)
if _, err = c.KubeClient.CronJobsGetter.CronJobs(c.Namespace).Get(jobName, metav1.GetOptions{}); err != nil {
return fmt.Errorf("could not fetch existing logical backup job: %v", err)
}
}
return nil
}

View File

@ -12,7 +12,7 @@ import (
"time"
"k8s.io/api/apps/v1beta1"
"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
policybeta1 "k8s.io/api/policy/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"

View File

@ -42,6 +42,8 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.PodEnvironmentConfigMap = fromCRD.Kubernetes.PodEnvironmentConfigMap
result.PodTerminateGracePeriod = time.Duration(fromCRD.Kubernetes.PodTerminateGracePeriod)
result.SpiloPrivileged = fromCRD.Kubernetes.SpiloPrivileged
result.SpiloFSGroup = fromCRD.Kubernetes.SpiloFSGroup
result.ClusterDomain = fromCRD.Kubernetes.ClusterDomain
result.WatchedNamespace = fromCRD.Kubernetes.WatchedNamespace
result.PDBNameFormat = fromCRD.Kubernetes.PDBNameFormat
result.SecretNameTemplate = fromCRD.Kubernetes.SecretNameTemplate
@ -106,5 +108,9 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.ScalyrCPULimit = fromCRD.Scalyr.ScalyrCPULimit
result.ScalyrMemoryLimit = fromCRD.Scalyr.ScalyrMemoryLimit
result.LogicalBackupSchedule = fromCRD.LogicalBackup.Schedule
result.LogicalBackupDockerImage = fromCRD.LogicalBackup.DockerImage
result.LogicalBackupS3Bucket = fromCRD.LogicalBackup.S3Bucket
return result
}

View File

@ -25,7 +25,9 @@ type Resources struct {
PodLabelWaitTimeout time.Duration `name:"pod_label_wait_timeout" default:"10m"`
PodDeletionWaitTimeout time.Duration `name:"pod_deletion_wait_timeout" default:"10m"`
PodTerminateGracePeriod time.Duration `name:"pod_terminate_grace_period" default:"5m"`
SpiloFSGroup *int64 `name:"spilo_fsgroup"`
PodPriorityClassName string `name:"pod_priority_class_name"`
ClusterDomain string `name:"cluster_domain" default:"cluster.local"`
SpiloPrivileged bool `name:"spilo_privileged" default:"false"`
ClusterLabels map[string]string `name:"cluster_labels" default:"application:spilo"`
InheritedLabels []string `name:"inherited_labels" default:""`
@ -66,16 +68,24 @@ type Scalyr struct {
ScalyrMemoryLimit string `name:"scalyr_memory_limit" default:"1Gi"`
}
// LogicalBackup
type LogicalBackup struct {
LogicalBackupSchedule string `name:"logical_backup_schedule" default:"30 00 * * *"`
LogicalBackupDockerImage string `name:"logical_backup_docker_image" default:"registry.opensource.zalan.do/acid/logical-backup"`
LogicalBackupS3Bucket string `name:"logical_backup_s3_bucket" default:""`
}
// Config describes operator config
type Config struct {
CRD
Resources
Auth
Scalyr
LogicalBackup
WatchedNamespace string `name:"watched_namespace"` // special values: "*" means 'watch all namespaces', the empty string "" means 'watch a namespace where operator is deployed to'
EtcdHost string `name:"etcd_host" default:""` // special values: the empty string "" means Patroni will use k8s as a DCS
DockerImage string `name:"docker_image" default:"registry.opensource.zalan.do/acid/spilo-cdp-10:1.4-p8"`
DockerImage string `name:"docker_image" default:"registry.opensource.zalan.do/acid/spilo-11:1.5-p7"`
Sidecars map[string]string `name:"sidecar_docker_images"`
// default name `operator` enables backward compatibility with the older ServiceAccountName field
PodServiceAccountName string `name:"pod_service_account_name" default:"operator"`

View File

@ -6,8 +6,11 @@ import (
b64 "encoding/base64"
batchv1beta1 "k8s.io/api/batch/v1beta1"
clientbatchv1beta1 "k8s.io/client-go/kubernetes/typed/batch/v1beta1"
"github.com/zalando/postgres-operator/pkg/util/constants"
"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
policybeta1 "k8s.io/api/policy/v1beta1"
apiextclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
apiextbeta1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1"
@ -40,6 +43,7 @@ type KubernetesClient struct {
rbacv1beta1.RoleBindingsGetter
policyv1beta1.PodDisruptionBudgetsGetter
apiextbeta1.CustomResourceDefinitionsGetter
clientbatchv1beta1.CronJobsGetter
RESTClient rest.Interface
AcidV1ClientSet *acidv1client.Clientset
@ -101,6 +105,7 @@ func NewFromConfig(cfg *rest.Config) (KubernetesClient, error) {
kubeClient.PodDisruptionBudgetsGetter = client.PolicyV1beta1()
kubeClient.RESTClient = client.CoreV1().RESTClient()
kubeClient.RoleBindingsGetter = client.RbacV1beta1()
kubeClient.CronJobsGetter = client.BatchV1beta1()
apiextClient, err := apiextclient.NewForConfig(cfg)
if err != nil {
@ -159,6 +164,28 @@ func SamePDB(cur, new *policybeta1.PodDisruptionBudget) (match bool, reason stri
return
}
func getJobImage(cronJob *batchv1beta1.CronJob) string {
return cronJob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Image
}
// SameLogicalBackupJob compares Specs of logical backup cron jobs
func SameLogicalBackupJob(cur, new *batchv1beta1.CronJob) (match bool, reason string) {
if cur.Spec.Schedule != new.Spec.Schedule {
return false, fmt.Sprintf("new job's schedule %q doesn't match the current one %q",
new.Spec.Schedule, cur.Spec.Schedule)
}
newImage := getJobImage(new)
curImage := getJobImage(cur)
if newImage != curImage {
return false, fmt.Sprintf("new job's image %q doesn't match the current one %q",
newImage, curImage)
}
return true, ""
}
func (c *mockSecret) Get(name string, options metav1.GetOptions) (*v1.Secret, error) {
if name != "infrastructureroles-test" {
return nil, fmt.Errorf("NotFound")

View File

@ -82,15 +82,15 @@ function clean_up(){
function start_minikube(){
echo "==== START MINIKUBE ==== "
echo "==== START MINIKUBE ===="
echo "May take a few minutes ..."
minikube start
kubectl config set-context minikube
echo "==== MINIKUBE STATUS ==== "
echo "==== MINIKUBE STATUS ===="
minikube status
echo ""
}
@ -133,7 +133,7 @@ function deploy_self_built_image() {
function start_operator(){
echo "==== START OPERATOR ==== "
echo "==== START OPERATOR ===="
echo "Certain operations may be retried multiple times..."
# the order of resource initialization is significant