Merge branch 'master' of https://github.com/zalando/postgres-operator into standby
This commit is contained in:
		
						commit
						14570dbc02
					
				|  | @ -0,0 +1,3 @@ | |||
| [flake8] | ||||
| exclude=.git,__pycache__ | ||||
| max-line-length=120 | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										17
									
								
								Makefile
								
								
								
								
							
							
						
						
									
										17
									
								
								Makefile
								
								
								
								
							|  | @ -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 | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -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 }} | ||||
|  |  | |||
|  | @ -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 }} | ||||
|  |  | |||
|  | @ -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 }} | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 }} | ||||
|  |  | |||
|  | @ -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 }} | ||||
|  | @ -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" . }} | ||||
|  |  | |||
|  | @ -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: "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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"] | ||||
|  | @ -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)) | ||||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
							
								
								
									
										15
									
								
								docs/user.md
								
								
								
								
							
							
						
						
									
										15
									
								
								docs/user.md
								
								
								
								
							|  | @ -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. | ||||
|  |  | |||
|  | @ -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"] | ||||
|  | @ -0,0 +1,6 @@ | |||
| kind: Cluster | ||||
| apiVersion: kind.sigs.k8s.io/v1alpha3 | ||||
| nodes: | ||||
| - role: control-plane | ||||
| - role: worker | ||||
| - role: worker | ||||
|  | @ -0,0 +1,3 @@ | |||
| kubernetes==9.0.0 | ||||
| timeout_decorator==0.4.1 | ||||
| pyyaml==5.1 | ||||
|  | @ -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 "$@" | ||||
|  | @ -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() | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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: "" | ||||
|  |  | |||
|  | @ -17,7 +17,6 @@ spec: | |||
|     # role for application foo | ||||
|     foo_user: [] | ||||
| 
 | ||||
|    | ||||
|   #databases: name->owner | ||||
|   databases: | ||||
|     foo: zalando | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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: "" | ||||
|  |  | |||
|  | @ -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"` | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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{}) | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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{ | ||||
|  |  | |||
|  | @ -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] | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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"` | ||||
|  |  | |||
|  | @ -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") | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue