Merge branch 'master' into gh-pages
This commit is contained in:
commit
191e7878b3
|
|
@ -9,7 +9,7 @@ assignees: ''
|
|||
|
||||
Please, answer some short questions which should help us to understand your problem / question better?
|
||||
|
||||
- **Which image of the operator are you using?** e.g. registry.opensource.zalan.do/acid/postgres-operator:v1.6.3
|
||||
- **Which image of the operator are you using?** e.g. registry.opensource.zalan.do/acid/postgres-operator:v1.7.0
|
||||
- **Where do you run it - cloud or metal? Kubernetes or OpenShift?** [AWS K8s | GCP ... | Bare Metal K8s]
|
||||
- **Are you running Postgres Operator in production?** [yes | no]
|
||||
- **Type of issue?** [Bug report, question, feature request, etc.]
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ We introduce the major version into the backup path to smoothen the [major versi
|
|||
The new operator configuration can set a compatibility flag *enable_spilo_wal_path_compat* to make Spilo look for wal segments in the current path but also old format paths.
|
||||
This comes at potential performance costs and should be disabled after a few days.
|
||||
|
||||
The newest Spilo 13 image is: `registry.opensource.zalan.do/acid/spilo-13:2.0-p7`
|
||||
The newest Spilo 13 image is: `registry.opensource.zalan.do/acid/spilo-13:2.1-p1`
|
||||
|
||||
The last Spilo 12 image is: `registry.opensource.zalan.do/acid/spilo-12:1.6-p5`
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
apiVersion: v1
|
||||
name: postgres-operator-ui
|
||||
version: 1.6.3
|
||||
appVersion: 1.6.3
|
||||
version: 1.7.0
|
||||
appVersion: 1.7.0
|
||||
home: https://github.com/zalando/postgres-operator
|
||||
description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience
|
||||
keywords:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,34 @@
|
|||
apiVersion: v1
|
||||
entries:
|
||||
postgres-operator-ui:
|
||||
- apiVersion: v1
|
||||
appVersion: 1.7.0
|
||||
created: "2021-08-27T10:23:17.723412079+02:00"
|
||||
description: Postgres Operator UI provides a graphical interface for a convenient
|
||||
database-as-a-service user experience
|
||||
digest: ad08ee5fe31bb2e7c3cc1299c2e778511a3c05305bc17357404b2615b32ea92a
|
||||
home: https://github.com/zalando/postgres-operator
|
||||
keywords:
|
||||
- postgres
|
||||
- operator
|
||||
- ui
|
||||
- cloud-native
|
||||
- patroni
|
||||
- spilo
|
||||
maintainers:
|
||||
- email: opensource@zalando.de
|
||||
name: Zalando
|
||||
name: postgres-operator-ui
|
||||
sources:
|
||||
- https://github.com/zalando/postgres-operator
|
||||
urls:
|
||||
- postgres-operator-ui-1.7.0.tgz
|
||||
version: 1.7.0
|
||||
- apiVersion: v1
|
||||
appVersion: 1.6.3
|
||||
created: "2021-05-27T19:04:33.425637932+02:00"
|
||||
description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience
|
||||
created: "2021-08-27T10:23:17.722255571+02:00"
|
||||
description: Postgres Operator UI provides a graphical interface for a convenient
|
||||
database-as-a-service user experience
|
||||
digest: 08b810aa632dcc719e4785ef184e391267f7c460caa99677f2d00719075aac78
|
||||
home: https://github.com/zalando/postgres-operator
|
||||
keywords:
|
||||
|
|
@ -25,8 +49,9 @@ entries:
|
|||
version: 1.6.3
|
||||
- apiVersion: v1
|
||||
appVersion: 1.6.2
|
||||
created: "2021-05-27T19:04:33.422124263+02:00"
|
||||
description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience
|
||||
created: "2021-08-27T10:23:17.721712848+02:00"
|
||||
description: Postgres Operator UI provides a graphical interface for a convenient
|
||||
database-as-a-service user experience
|
||||
digest: 14d1559bb0bd1e1e828f2daaaa6f6ac9ffc268d79824592c3589b55dd39241f6
|
||||
home: https://github.com/zalando/postgres-operator
|
||||
keywords:
|
||||
|
|
@ -47,8 +72,9 @@ entries:
|
|||
version: 1.6.2
|
||||
- apiVersion: v1
|
||||
appVersion: 1.6.1
|
||||
created: "2021-05-27T19:04:33.419640902+02:00"
|
||||
description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience
|
||||
created: "2021-08-27T10:23:17.721175629+02:00"
|
||||
description: Postgres Operator UI provides a graphical interface for a convenient
|
||||
database-as-a-service user experience
|
||||
digest: 3d321352f2f1e7bb7450aa8876e3d818aa9f9da9bd4250507386f0490f2c1969
|
||||
home: https://github.com/zalando/postgres-operator
|
||||
keywords:
|
||||
|
|
@ -69,8 +95,9 @@ entries:
|
|||
version: 1.6.1
|
||||
- apiVersion: v1
|
||||
appVersion: 1.6.0
|
||||
created: "2021-05-27T19:04:33.41788193+02:00"
|
||||
description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience
|
||||
created: "2021-08-27T10:23:17.720655498+02:00"
|
||||
description: Postgres Operator UI provides a graphical interface for a convenient
|
||||
database-as-a-service user experience
|
||||
digest: 1e0aa1e7db3c1daa96927ffbf6fdbcdb434562f961833cb5241ddbe132220ee4
|
||||
home: https://github.com/zalando/postgres-operator
|
||||
keywords:
|
||||
|
|
@ -91,8 +118,9 @@ entries:
|
|||
version: 1.6.0
|
||||
- apiVersion: v1
|
||||
appVersion: 1.5.0
|
||||
created: "2021-05-27T19:04:33.416056821+02:00"
|
||||
description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience
|
||||
created: "2021-08-27T10:23:17.720112359+02:00"
|
||||
description: Postgres Operator UI provides a graphical interface for a convenient
|
||||
database-as-a-service user experience
|
||||
digest: c91ea39e6d51d57f4048fb1b6ec53b40823f2690eb88e4e4f1a036367b9fdd61
|
||||
home: https://github.com/zalando/postgres-operator
|
||||
keywords:
|
||||
|
|
@ -111,4 +139,4 @@ entries:
|
|||
urls:
|
||||
- postgres-operator-ui-1.5.0.tgz
|
||||
version: 1.5.0
|
||||
generated: "2021-05-27T19:04:33.41380858+02:00"
|
||||
generated: "2021-08-27T10:23:17.719397521+02:00"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -7,6 +7,7 @@ metadata:
|
|||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
name: {{ template "postgres-operator-ui.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
|
|
@ -43,13 +44,13 @@ spec:
|
|||
- name: "APP_URL"
|
||||
value: "http://localhost:8081"
|
||||
- name: "OPERATOR_API_URL"
|
||||
value: {{ .Values.envs.operatorApiUrl }}
|
||||
value: {{ .Values.envs.operatorApiUrl | quote }}
|
||||
- name: "OPERATOR_CLUSTER_NAME_LABEL"
|
||||
value: {{ .Values.envs.operatorClusterNameLabel }}
|
||||
value: {{ .Values.envs.operatorClusterNameLabel | quote }}
|
||||
- name: "RESOURCES_VISIBLE"
|
||||
value: "{{ .Values.envs.resourcesVisible }}"
|
||||
value: {{ .Values.envs.resourcesVisible | quote }}
|
||||
- name: "TARGET_NAMESPACE"
|
||||
value: "{{ .Values.envs.targetNamespace }}"
|
||||
value: {{ .Values.envs.targetNamespace | quote }}
|
||||
- name: "TEAMS"
|
||||
value: |-
|
||||
[
|
||||
|
|
@ -75,3 +76,6 @@ spec:
|
|||
"11"
|
||||
]
|
||||
}
|
||||
{{- if .Values.extraEnvs }}
|
||||
{{- .Values.extraEnvs | toYaml | nindent 12 }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "postgres-operator-ui.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
|
|
@ -9,6 +12,7 @@ apiVersion: extensions/v1beta1
|
|||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ template "postgres-operator-ui.name" . }}
|
||||
helm.sh/chart: {{ template "postgres-operator-ui.chart" . }}
|
||||
|
|
@ -36,9 +40,18 @@ spec:
|
|||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ . }}
|
||||
{{ if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion -}}
|
||||
pathType: ImplementationSpecific
|
||||
backend:
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- else -}}
|
||||
backend:
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: {{ $svcPort }}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ metadata:
|
|||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
name: {{ template "postgres-operator-ui.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
spec:
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ apiVersion: v1
|
|||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "postgres-operator-ui.serviceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ template "postgres-operator-ui.name" . }}
|
||||
helm.sh/chart: {{ template "postgres-operator-ui.chart" . }}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ replicaCount: 1
|
|||
image:
|
||||
registry: registry.opensource.zalan.do
|
||||
repository: acid/postgres-operator-ui
|
||||
tag: v1.6.3
|
||||
tag: v1.7.0
|
||||
pullPolicy: "IfNotPresent"
|
||||
|
||||
# Optionally specify an array of imagePullSecrets.
|
||||
|
|
@ -48,6 +48,36 @@ envs:
|
|||
teams:
|
||||
- "acid"
|
||||
|
||||
# configure extra UI ENVs
|
||||
# Extra ENVs are writen in kubenertes format and added "as is" to the pod's env variables
|
||||
# https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/
|
||||
# https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#environment-variables
|
||||
# UI specific env variables can be found here: https://github.com/zalando/postgres-operator/blob/master/ui/operator_ui/main.py
|
||||
extraEnvs:
|
||||
[]
|
||||
# Exemple of settings to make snapshot view working in the ui when using AWS
|
||||
# - name: WALE_S3_ENDPOINT
|
||||
# value: https+path://s3.us-east-1.amazonaws.com:443
|
||||
# - name: SPILO_S3_BACKUP_PREFIX
|
||||
# value: spilo/
|
||||
# - name: AWS_ACCESS_KEY_ID
|
||||
# valueFrom:
|
||||
# secretKeyRef:
|
||||
# name: <postgres operator secret with AWS token>
|
||||
# key: AWS_ACCESS_KEY_ID
|
||||
# - name: AWS_SECRET_ACCESS_KEY
|
||||
# valueFrom:
|
||||
# secretKeyRef:
|
||||
# name: <postgres operator secret with AWS token>
|
||||
# key: AWS_SECRET_ACCESS_KEY
|
||||
# - name: AWS_DEFAULT_REGION
|
||||
# valueFrom:
|
||||
# secretKeyRef:
|
||||
# name: <postgres operator secret with AWS token>
|
||||
# key: AWS_DEFAULT_REGION
|
||||
# - name: SPILO_S3_BACKUP_BUCKET
|
||||
# value: <s3 bucket used by the operator>
|
||||
|
||||
# configure UI service
|
||||
service:
|
||||
type: "ClusterIP"
|
||||
|
|
@ -59,7 +89,8 @@ service:
|
|||
# configure UI ingress. If needed: "enabled: true"
|
||||
ingress:
|
||||
enabled: false
|
||||
annotations: {}
|
||||
annotations:
|
||||
{}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
apiVersion: v1
|
||||
name: postgres-operator
|
||||
version: 1.6.3
|
||||
appVersion: 1.6.3
|
||||
version: 1.7.0
|
||||
appVersion: 1.7.0
|
||||
home: https://github.com/zalando/postgres-operator
|
||||
description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes
|
||||
keywords:
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ spec:
|
|||
properties:
|
||||
docker_image:
|
||||
type: string
|
||||
default: "registry.opensource.zalan.do/acid/spilo-13:2.0-p7"
|
||||
default: "registry.opensource.zalan.do/acid/spilo-13:2.1-p1"
|
||||
enable_crd_validation:
|
||||
type: boolean
|
||||
default: true
|
||||
|
|
@ -173,6 +173,9 @@ spec:
|
|||
enable_init_containers:
|
||||
type: boolean
|
||||
default: true
|
||||
enable_cross_namespace_secret:
|
||||
type: boolean
|
||||
default: false
|
||||
enable_pod_antiaffinity:
|
||||
type: boolean
|
||||
default: false
|
||||
|
|
@ -392,12 +395,14 @@ spec:
|
|||
type: string
|
||||
wal_s3_bucket:
|
||||
type: string
|
||||
wal_az_storage_account:
|
||||
type: string
|
||||
logical_backup:
|
||||
type: object
|
||||
properties:
|
||||
logical_backup_docker_image:
|
||||
type: string
|
||||
default: "registry.opensource.zalan.do/acid/logical-backup:v1.6.3"
|
||||
default: "registry.opensource.zalan.do/acid/logical-backup:v1.7.0"
|
||||
logical_backup_google_application_credentials:
|
||||
type: string
|
||||
logical_backup_job_prefix:
|
||||
|
|
@ -532,7 +537,7 @@ spec:
|
|||
default: "pooler"
|
||||
connection_pooler_image:
|
||||
type: string
|
||||
default: "registry.opensource.zalan.do/acid/pgbouncer:master-16"
|
||||
default: "registry.opensource.zalan.do/acid/pgbouncer:master-18"
|
||||
connection_pooler_max_db_connections:
|
||||
type: integer
|
||||
default: 60
|
||||
|
|
|
|||
|
|
@ -223,6 +223,97 @@ spec:
|
|||
items:
|
||||
type: string
|
||||
pattern: '^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ *$'
|
||||
nodeAffinity:
|
||||
type: object
|
||||
properties:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- weight
|
||||
- preference
|
||||
properties:
|
||||
preference:
|
||||
type: object
|
||||
properties:
|
||||
matchExpressions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
matchFields:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
weight:
|
||||
format: int32
|
||||
type: integer
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
type: object
|
||||
required:
|
||||
- nodeSelectorTerms
|
||||
properties:
|
||||
nodeSelectorTerms:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
matchExpressions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
matchFields:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
numberOfInstances:
|
||||
type: integer
|
||||
minimum: 0
|
||||
|
|
@ -303,6 +394,8 @@ spec:
|
|||
type: boolean
|
||||
defaultRoles:
|
||||
type: boolean
|
||||
secretNamespace:
|
||||
type: string
|
||||
replicaLoadBalancer: # deprecated
|
||||
type: boolean
|
||||
resources:
|
||||
|
|
@ -396,97 +489,6 @@ spec:
|
|||
type: string
|
||||
caSecretName:
|
||||
type: string
|
||||
nodeAffinity:
|
||||
type: object
|
||||
properties:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- weight
|
||||
- preference
|
||||
properties:
|
||||
preference:
|
||||
type: object
|
||||
properties:
|
||||
matchExpressions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
matchFields:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
weight:
|
||||
format: int32
|
||||
type: integer
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
type: object
|
||||
required:
|
||||
- nodeSelectorTerms
|
||||
properties:
|
||||
nodeSelectorTerms:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
matchExpressions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
matchFields:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
tolerations:
|
||||
type: array
|
||||
items:
|
||||
|
|
@ -559,6 +561,24 @@ spec:
|
|||
properties:
|
||||
iops:
|
||||
type: integer
|
||||
selector:
|
||||
type: object
|
||||
properties:
|
||||
matchExpressions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
matchLabels:
|
||||
type: object
|
||||
size:
|
||||
type: string
|
||||
pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$'
|
||||
|
|
|
|||
|
|
@ -1,10 +1,33 @@
|
|||
apiVersion: v1
|
||||
entries:
|
||||
postgres-operator:
|
||||
- apiVersion: v1
|
||||
appVersion: 1.7.0
|
||||
created: "2021-08-27T10:21:42.643185124+02:00"
|
||||
description: Postgres Operator creates and manages PostgreSQL clusters running
|
||||
in Kubernetes
|
||||
digest: 1c4a1d289188ef72e409892fd2b86c008a37420af04a9796a8829ff84ab09e61
|
||||
home: https://github.com/zalando/postgres-operator
|
||||
keywords:
|
||||
- postgres
|
||||
- operator
|
||||
- cloud-native
|
||||
- patroni
|
||||
- spilo
|
||||
maintainers:
|
||||
- email: opensource@zalando.de
|
||||
name: Zalando
|
||||
name: postgres-operator
|
||||
sources:
|
||||
- https://github.com/zalando/postgres-operator
|
||||
urls:
|
||||
- postgres-operator-1.7.0.tgz
|
||||
version: 1.7.0
|
||||
- apiVersion: v1
|
||||
appVersion: 1.6.3
|
||||
created: "2021-05-27T19:04:25.199523943+02:00"
|
||||
description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes
|
||||
created: "2021-08-27T10:21:42.640069574+02:00"
|
||||
description: Postgres Operator creates and manages PostgreSQL clusters running
|
||||
in Kubernetes
|
||||
digest: ea08f991bf23c9ad114bca98ebcbe3e2fa15beab163061399394905eaee89b35
|
||||
home: https://github.com/zalando/postgres-operator
|
||||
keywords:
|
||||
|
|
@ -24,8 +47,9 @@ entries:
|
|||
version: 1.6.3
|
||||
- apiVersion: v1
|
||||
appVersion: 1.6.2
|
||||
created: "2021-05-27T19:04:25.198182197+02:00"
|
||||
description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes
|
||||
created: "2021-08-27T10:21:42.638502739+02:00"
|
||||
description: Postgres Operator creates and manages PostgreSQL clusters running
|
||||
in Kubernetes
|
||||
digest: d886f8a0879ca07d1e5246ee7bc55710e1c872f3977280fe495db6fc2057a7f4
|
||||
home: https://github.com/zalando/postgres-operator
|
||||
keywords:
|
||||
|
|
@ -45,8 +69,9 @@ entries:
|
|||
version: 1.6.2
|
||||
- apiVersion: v1
|
||||
appVersion: 1.6.1
|
||||
created: "2021-05-27T19:04:25.19687586+02:00"
|
||||
description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes
|
||||
created: "2021-08-27T10:21:42.636936467+02:00"
|
||||
description: Postgres Operator creates and manages PostgreSQL clusters running
|
||||
in Kubernetes
|
||||
digest: 4ba5972cd486dcaa2d11c5613a6f97f6b7b831822e610fe9e10a57ea1db23556
|
||||
home: https://github.com/zalando/postgres-operator
|
||||
keywords:
|
||||
|
|
@ -66,8 +91,9 @@ entries:
|
|||
version: 1.6.1
|
||||
- apiVersion: v1
|
||||
appVersion: 1.6.0
|
||||
created: "2021-05-27T19:04:25.195600766+02:00"
|
||||
description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes
|
||||
created: "2021-08-27T10:21:42.63533527+02:00"
|
||||
description: Postgres Operator creates and manages PostgreSQL clusters running
|
||||
in Kubernetes
|
||||
digest: f52149718ea364f46b4b9eec9a65f6253ad182bb78df541d14cd5277b9c8a8c3
|
||||
home: https://github.com/zalando/postgres-operator
|
||||
keywords:
|
||||
|
|
@ -87,8 +113,9 @@ entries:
|
|||
version: 1.6.0
|
||||
- apiVersion: v1
|
||||
appVersion: 1.5.0
|
||||
created: "2021-05-27T19:04:25.193985032+02:00"
|
||||
description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes
|
||||
created: "2021-08-27T10:21:42.632932257+02:00"
|
||||
description: Postgres Operator creates and manages PostgreSQL clusters running
|
||||
in Kubernetes
|
||||
digest: 198351d5db52e65cdf383d6f3e1745d91ac1e2a01121f8476f8b1be728b09531
|
||||
home: https://github.com/zalando/postgres-operator
|
||||
keywords:
|
||||
|
|
@ -106,4 +133,4 @@ entries:
|
|||
urls:
|
||||
- postgres-operator-1.5.0.tgz
|
||||
version: 1.5.0
|
||||
generated: "2021-05-27T19:04:25.191897769+02:00"
|
||||
generated: "2021-08-27T10:21:42.631372502+02:00"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -51,3 +51,24 @@ Create chart name and version as used by the chart label.
|
|||
{{- define "postgres-operator.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Flatten nested config options when ConfigMap is used as ConfigTarget
|
||||
*/}}
|
||||
{{- define "flattenValuesForConfigMap" }}
|
||||
{{- range $key, $value := . }}
|
||||
{{- if or (kindIs "string" $value) (kindIs "int" $value) }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{- end }}
|
||||
{{- if kindIs "slice" $value }}
|
||||
{{ $key }}: {{ join "," $value | quote }}
|
||||
{{- end }}
|
||||
{{- if kindIs "map" $value }}
|
||||
{{- $list := list }}
|
||||
{{- range $subKey, $subValue := $value }}
|
||||
{{- $list = append $list (printf "%s:%s" $subKey $subValue) }}
|
||||
{{ $key }}: {{ join "," $list | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ apiVersion: v1
|
|||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ template "postgres-operator.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ template "postgres-operator.name" . }}
|
||||
helm.sh/chart: {{ template "postgres-operator.chart" . }}
|
||||
|
|
@ -13,16 +14,16 @@ data:
|
|||
pod_priority_class_name: {{ .Values.podPriorityClassName }}
|
||||
{{- end }}
|
||||
pod_service_account_name: {{ include "postgres-pod.serviceAccountName" . }}
|
||||
{{ toYaml .Values.configGeneral | indent 2 }}
|
||||
{{ toYaml .Values.configUsers | indent 2 }}
|
||||
{{ toYaml .Values.configMajorVersionUpgrade | indent 2 }}
|
||||
{{ toYaml .Values.configKubernetes | indent 2 }}
|
||||
{{ toYaml .Values.configTimeouts | indent 2 }}
|
||||
{{ toYaml .Values.configLoadBalancer | indent 2 }}
|
||||
{{ toYaml .Values.configAwsOrGcp | indent 2 }}
|
||||
{{ toYaml .Values.configLogicalBackup | indent 2 }}
|
||||
{{ toYaml .Values.configDebug | indent 2 }}
|
||||
{{ toYaml .Values.configLoggingRestApi | indent 2 }}
|
||||
{{ toYaml .Values.configTeamsApi | indent 2 }}
|
||||
{{ toYaml .Values.configConnectionPooler | indent 2 }}
|
||||
{{- include "flattenValuesForConfigMap" .Values.configGeneral | indent 2 }}
|
||||
{{- include "flattenValuesForConfigMap" .Values.configUsers | indent 2 }}
|
||||
{{- include "flattenValuesForConfigMap" .Values.configMajorVersionUpgrade | indent 2 }}
|
||||
{{- include "flattenValuesForConfigMap" .Values.configKubernetes | indent 2 }}
|
||||
{{- include "flattenValuesForConfigMap" .Values.configTimeouts | indent 2 }}
|
||||
{{- include "flattenValuesForConfigMap" .Values.configLoadBalancer | indent 2 }}
|
||||
{{- include "flattenValuesForConfigMap" .Values.configAwsOrGcp | indent 2 }}
|
||||
{{- include "flattenValuesForConfigMap" .Values.configLogicalBackup | indent 2 }}
|
||||
{{- include "flattenValuesForConfigMap" .Values.configDebug | indent 2 }}
|
||||
{{- include "flattenValuesForConfigMap" .Values.configLoggingRestApi | indent 2 }}
|
||||
{{- include "flattenValuesForConfigMap" .Values.configTeamsApi | indent 2 }}
|
||||
{{- include "flattenValuesForConfigMap" .Values.configConnectionPooler | indent 2 }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ metadata:
|
|||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
name: {{ template "postgres-operator.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ apiVersion: "acid.zalan.do/v1"
|
|||
kind: OperatorConfiguration
|
||||
metadata:
|
||||
name: {{ template "postgres-operator.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ template "postgres-operator.name" . }}
|
||||
helm.sh/chart: {{ template "postgres-operator.chart" . }}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ metadata:
|
|||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
name: {{ .Values.podPriorityClassName }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
preemptionPolicy: PreemptLowerPriority
|
||||
globalDefault: false
|
||||
value: 1000000
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ metadata:
|
|||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
name: {{ template "postgres-operator.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ apiVersion: v1
|
|||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "postgres-operator.serviceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ template "postgres-operator.name" . }}
|
||||
helm.sh/chart: {{ template "postgres-operator.chart" . }}
|
||||
|
|
|
|||
|
|
@ -1,403 +0,0 @@
|
|||
image:
|
||||
registry: registry.opensource.zalan.do
|
||||
repository: acid/postgres-operator
|
||||
tag: v1.6.3
|
||||
pullPolicy: "IfNotPresent"
|
||||
|
||||
# Optionally specify an array of imagePullSecrets.
|
||||
# Secrets must be manually created in the namespace.
|
||||
# ref: https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod
|
||||
# imagePullSecrets:
|
||||
# - name: myRegistryKeySecretName
|
||||
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
|
||||
configTarget: "OperatorConfigurationCRD"
|
||||
|
||||
# general top-level configuration parameters
|
||||
configGeneral:
|
||||
# choose if deployment creates/updates CRDs with OpenAPIV3Validation
|
||||
enable_crd_validation: true
|
||||
# update only the statefulsets without immediately doing the rolling update
|
||||
enable_lazy_spilo_upgrade: false
|
||||
# set the PGVERSION env var instead of providing the version via postgresql.bin_dir in SPILO_CONFIGURATION
|
||||
enable_pgversion_env_var: true
|
||||
# start any new database pod without limitations on shm memory
|
||||
enable_shm_volume: true
|
||||
# enables backwards compatible path between Spilo 12 and Spilo 13 images
|
||||
enable_spilo_wal_path_compat: false
|
||||
# etcd connection string for Patroni. Empty uses K8s-native DCS.
|
||||
etcd_host: ""
|
||||
# Select if setup uses endpoints (default), or configmaps to manage leader (DCS=k8s)
|
||||
# kubernetes_use_configmaps: false
|
||||
# Spilo docker image
|
||||
docker_image: registry.opensource.zalan.do/acid/spilo-13:2.0-p7
|
||||
# min number of instances in Postgres cluster. -1 = no limit
|
||||
min_instances: -1
|
||||
# max number of instances in Postgres cluster. -1 = no limit
|
||||
max_instances: -1
|
||||
# period between consecutive repair requests
|
||||
repair_period: 5m
|
||||
# period between consecutive sync requests
|
||||
resync_period: 30m
|
||||
# can prevent certain cases of memory overcommitment
|
||||
# set_memory_request_to_limit: false
|
||||
|
||||
# map of sidecar names to docker images
|
||||
# sidecar_docker_images
|
||||
# example: "exampleimage:exampletag"
|
||||
|
||||
# number of routines the operator spawns to process requests concurrently
|
||||
workers: 8
|
||||
|
||||
# parameters describing Postgres users
|
||||
configUsers:
|
||||
# postgres username used for replication between instances
|
||||
replication_username: standby
|
||||
# postgres superuser name to be created by initdb
|
||||
super_username: postgres
|
||||
|
||||
configMajorVersionUpgrade:
|
||||
# "off": no upgrade, "manual": manifest triggers action, "full": minimal version violation triggers too
|
||||
major_version_upgrade_mode: "off"
|
||||
# minimal Postgres major version that will not automatically be upgraded
|
||||
minimal_major_version: "9.5"
|
||||
# target Postgres major version when upgrading clusters automatically
|
||||
target_major_version: "13"
|
||||
|
||||
configKubernetes:
|
||||
# list of additional capabilities for postgres container
|
||||
# additional_pod_capabilities:
|
||||
# - "SYS_NICE"
|
||||
|
||||
# default DNS domain of K8s cluster where operator is running
|
||||
cluster_domain: cluster.local
|
||||
# additional labels assigned to the cluster objects
|
||||
cluster_labels:
|
||||
application: spilo
|
||||
# label assigned to Kubernetes objects created by the operator
|
||||
cluster_name_label: cluster-name
|
||||
# additional annotations to add to every database pod
|
||||
# custom_pod_annotations:
|
||||
# keya: valuea
|
||||
# keyb: valueb
|
||||
|
||||
# key name for annotation that compares manifest value with current date
|
||||
# delete_annotation_date_key: "delete-date"
|
||||
|
||||
# key name for annotation that compares manifest value with cluster name
|
||||
# delete_annotation_name_key: "delete-clustername"
|
||||
|
||||
# list of annotations propagated from cluster manifest to statefulset and deployment
|
||||
# downscaler_annotations:
|
||||
# - deployment-time
|
||||
# - downscaler/*
|
||||
|
||||
# enables initContainers to run actions before Spilo is started
|
||||
enable_init_containers: true
|
||||
# toggles pod anti affinity on the Postgres pods
|
||||
enable_pod_antiaffinity: false
|
||||
# toggles PDB to set to MinAvailabe 0 or 1
|
||||
enable_pod_disruption_budget: true
|
||||
# enables sidecar containers to run alongside Spilo in the same pod
|
||||
enable_sidecars: true
|
||||
# namespaced name of the secret containing infrastructure roles names and passwords
|
||||
# infrastructure_roles_secret_name: postgresql-infrastructure-roles
|
||||
|
||||
# list of annotation keys that can be inherited from the cluster manifest
|
||||
# inherited_annotations:
|
||||
# - owned-by
|
||||
|
||||
# list of label keys that can be inherited from the cluster manifest
|
||||
# inherited_labels:
|
||||
# - application
|
||||
# - environment
|
||||
|
||||
# timeout for successful migration of master pods from unschedulable node
|
||||
# master_pod_move_timeout: 20m
|
||||
|
||||
# set of labels that a running and active node should possess to be considered ready
|
||||
# node_readiness_label:
|
||||
# status: ready
|
||||
|
||||
# namespaced name of the secret containing the OAuth2 token to pass to the teams API
|
||||
# oauth_token_secret_name: postgresql-operator
|
||||
|
||||
# defines the template for PDB (Pod Disruption Budget) names
|
||||
pdb_name_format: "postgres-{cluster}-pdb"
|
||||
# override topology key for pod anti affinity
|
||||
pod_antiaffinity_topology_key: "kubernetes.io/hostname"
|
||||
# namespaced name of the ConfigMap with environment variables to populate on every pod
|
||||
# pod_environment_configmap: "default/my-custom-config"
|
||||
# name of the Secret (in cluster namespace) with environment variables to populate on every pod
|
||||
# pod_environment_secret: "my-custom-secret"
|
||||
|
||||
# specify the pod management policy of stateful sets of Postgres clusters
|
||||
pod_management_policy: "ordered_ready"
|
||||
# label assigned to the Postgres pods (and services/endpoints)
|
||||
pod_role_label: spilo-role
|
||||
# service account definition as JSON/YAML string to be used by postgres cluster pods
|
||||
# pod_service_account_definition: ""
|
||||
|
||||
# role binding definition as JSON/YAML string to be used by pod service account
|
||||
# pod_service_account_role_binding_definition: ""
|
||||
|
||||
# Postgres pods are terminated forcefully after this timeout
|
||||
pod_terminate_grace_period: 5m
|
||||
# template for database user secrets generated by the operator
|
||||
secret_name_template: "{username}.{cluster}.credentials.{tprkind}.{tprgroup}"
|
||||
# set user and group for the spilo container (required to run Spilo as non-root process)
|
||||
# spilo_runasuser: "101"
|
||||
# spilo_runasgroup: "103"
|
||||
# group ID with write-access to volumes (required to run Spilo as non-root process)
|
||||
# spilo_fsgroup: 103
|
||||
|
||||
# whether the Spilo container should run in privileged mode
|
||||
spilo_privileged: false
|
||||
# whether the Spilo container should run with additional permissions other than parent.
|
||||
# required by cron which needs setuid
|
||||
spilo_allow_privilege_escalation: true
|
||||
# storage resize strategy, available options are: ebs, pvc, off
|
||||
storage_resize_mode: pvc
|
||||
# operator watches for postgres objects in the given namespace
|
||||
watched_namespace: "*" # listen to all namespaces
|
||||
|
||||
# configure resource requests for the Postgres pods
|
||||
configPostgresPodResources:
|
||||
# CPU limits for the postgres containers
|
||||
default_cpu_limit: "1"
|
||||
# CPU request value for the postgres containers
|
||||
default_cpu_request: 100m
|
||||
# memory limits for the postgres containers
|
||||
default_memory_limit: 500Mi
|
||||
# memory request value for the postgres containers
|
||||
default_memory_request: 100Mi
|
||||
# hard CPU minimum required to properly run a Postgres cluster
|
||||
min_cpu_limit: 250m
|
||||
# hard memory minimum required to properly run a Postgres cluster
|
||||
min_memory_limit: 250Mi
|
||||
|
||||
# timeouts related to some operator actions
|
||||
configTimeouts:
|
||||
# timeout when waiting for the Postgres pods to be deleted
|
||||
pod_deletion_wait_timeout: 10m
|
||||
# timeout when waiting for pod role and cluster labels
|
||||
pod_label_wait_timeout: 10m
|
||||
# interval between consecutive attempts waiting for postgresql CRD to be created
|
||||
ready_wait_interval: 3s
|
||||
# timeout for the complete postgres CRD creation
|
||||
ready_wait_timeout: 30s
|
||||
# interval to wait between consecutive attempts to check for some K8s resources
|
||||
resource_check_interval: 3s
|
||||
# timeout when waiting for the presence of a certain K8s resource (e.g. Sts, PDB)
|
||||
resource_check_timeout: 10m
|
||||
|
||||
# configure behavior of load balancers
|
||||
configLoadBalancer:
|
||||
# DNS zone for cluster DNS name when load balancer is configured for cluster
|
||||
db_hosted_zone: db.example.com
|
||||
# annotations to apply to service when load balancing is enabled
|
||||
# custom_service_annotations:
|
||||
# keyx: valuez
|
||||
# keya: valuea
|
||||
|
||||
# toggles service type load balancer pointing to the master pod of the cluster
|
||||
enable_master_load_balancer: false
|
||||
# toggles service type load balancer pointing to the replica pod of the cluster
|
||||
enable_replica_load_balancer: false
|
||||
# define external traffic policy for the load balancer
|
||||
external_traffic_policy: "Cluster"
|
||||
# defines the DNS name string template for the master load balancer cluster
|
||||
master_dns_name_format: "{cluster}.{team}.{hostedzone}"
|
||||
# defines the DNS name string template for the replica load balancer cluster
|
||||
replica_dns_name_format: "{cluster}-repl.{team}.{hostedzone}"
|
||||
|
||||
# options to aid debugging of the operator itself
|
||||
configDebug:
|
||||
# toggles verbose debug logs from the operator
|
||||
debug_logging: true
|
||||
# toggles operator functionality that require access to the postgres database
|
||||
enable_database_access: true
|
||||
|
||||
# parameters affecting logging and REST API listener
|
||||
configLoggingRestApi:
|
||||
# REST API listener listens to this port
|
||||
api_port: 8080
|
||||
# number of entries in the cluster history ring buffer
|
||||
cluster_history_entries: 1000
|
||||
# number of lines in the ring buffer used to store cluster logs
|
||||
ring_log_lines: 100
|
||||
|
||||
# configure interaction with non-Kubernetes objects from AWS or GCP
|
||||
configAwsOrGcp:
|
||||
# Additional Secret (aws or gcp credentials) to mount in the pod
|
||||
# additional_secret_mount: "some-secret-name"
|
||||
|
||||
# Path to mount the above Secret in the filesystem of the container(s)
|
||||
# additional_secret_mount_path: "/some/dir"
|
||||
|
||||
# AWS region used to store ESB volumes
|
||||
aws_region: eu-central-1
|
||||
|
||||
# enable automatic migration on AWS from gp2 to gp3 volumes
|
||||
enable_ebs_gp3_migration: false
|
||||
# defines maximum volume size in GB until which auto migration happens
|
||||
# enable_ebs_gp3_migration_max_size: 1000
|
||||
|
||||
# GCP credentials that will be used by the operator / pods
|
||||
# gcp_credentials: ""
|
||||
|
||||
# AWS IAM role to supply in the iam.amazonaws.com/role annotation of Postgres pods
|
||||
# kube_iam_role: ""
|
||||
|
||||
# S3 bucket to use for shipping postgres daily logs
|
||||
# log_s3_bucket: ""
|
||||
|
||||
# GCS bucket to use for shipping WAL segments with WAL-E
|
||||
# wal_gs_bucket: ""
|
||||
|
||||
# S3 bucket to use for shipping WAL segments with WAL-E
|
||||
# wal_s3_bucket: ""
|
||||
|
||||
# configure K8s cron job managed by the operator
|
||||
configLogicalBackup:
|
||||
# image for pods of the logical backup job (example runs pg_dumpall)
|
||||
logical_backup_docker_image: "registry.opensource.zalan.do/acid/logical-backup:v1.6.3"
|
||||
# path of google cloud service account json file
|
||||
# logical_backup_google_application_credentials: ""
|
||||
|
||||
# prefix for the backup job name
|
||||
logical_backup_job_prefix: "logical-backup-"
|
||||
# storage provider - either "s3" or "gcs"
|
||||
logical_backup_provider: "s3"
|
||||
# S3 Access Key ID
|
||||
logical_backup_s3_access_key_id: ""
|
||||
# S3 bucket to store backup results
|
||||
logical_backup_s3_bucket: "my-bucket-url"
|
||||
# S3 region of bucket
|
||||
logical_backup_s3_region: ""
|
||||
# S3 endpoint url when not using AWS
|
||||
logical_backup_s3_endpoint: ""
|
||||
# S3 Secret Access Key
|
||||
logical_backup_s3_secret_access_key: ""
|
||||
# S3 server side encryption
|
||||
logical_backup_s3_sse: "AES256"
|
||||
# backup schedule in the cron format
|
||||
logical_backup_schedule: "30 00 * * *"
|
||||
|
||||
# automate creation of human users with teams API service
|
||||
configTeamsApi:
|
||||
# team_admin_role will have the rights to grant roles coming from PG manifests
|
||||
enable_admin_role_for_users: true
|
||||
# operator watches for PostgresTeam CRs to assign additional teams and members to clusters
|
||||
enable_postgres_team_crd: false
|
||||
# toogle to create additional superuser teams from PostgresTeam CRs
|
||||
enable_postgres_team_crd_superusers: false
|
||||
# toggle to automatically rename roles of former team members and deny LOGIN
|
||||
enable_team_member_deprecation: false
|
||||
# toggle to grant superuser to team members created from the Teams API
|
||||
enable_team_superuser: false
|
||||
# toggles usage of the Teams API by the operator
|
||||
enable_teams_api: false
|
||||
# should contain a URL to use for authentication (username and token)
|
||||
# pam_configuration: ""
|
||||
|
||||
# operator will add all team member roles to this group and add a pg_hba line
|
||||
pam_role_name: zalandos
|
||||
# List of teams which members need the superuser role in each Postgres cluster
|
||||
postgres_superuser_teams:
|
||||
- postgres_superusers
|
||||
# List of roles that cannot be overwritten by an application, team or infrastructure role
|
||||
protected_role_names:
|
||||
- admin
|
||||
# Suffix to add if members are removed from TeamsAPI or PostgresTeam CRD
|
||||
role_deletion_suffix: "_deleted"
|
||||
# role name to grant to team members created from the Teams API
|
||||
team_admin_role: admin
|
||||
# postgres config parameters to apply to each team member role
|
||||
team_api_role_configuration:
|
||||
log_statement: all
|
||||
# URL of the Teams API service
|
||||
# teams_api_url: http://fake-teams-api.default.svc.cluster.local
|
||||
|
||||
configConnectionPooler:
|
||||
# db schema to install lookup function into
|
||||
connection_pooler_schema: "pooler"
|
||||
# db user for pooler to use
|
||||
connection_pooler_user: "pooler"
|
||||
# docker image
|
||||
connection_pooler_image: "registry.opensource.zalan.do/acid/pgbouncer:master-16"
|
||||
# max db connections the pooler should hold
|
||||
connection_pooler_max_db_connections: 60
|
||||
# default pooling mode
|
||||
connection_pooler_mode: "transaction"
|
||||
# number of pooler instances
|
||||
connection_pooler_number_of_instances: 2
|
||||
# default resources
|
||||
connection_pooler_default_cpu_request: 500m
|
||||
connection_pooler_default_memory_request: 100Mi
|
||||
connection_pooler_default_cpu_limit: "1"
|
||||
connection_pooler_default_memory_limit: 100Mi
|
||||
|
||||
rbac:
|
||||
# Specifies whether RBAC resources should be created
|
||||
create: true
|
||||
|
||||
crd:
|
||||
# Specifies whether custom resource definitions should be created
|
||||
# When using helm3, this is ignored; instead use "--skip-crds" to skip.
|
||||
create: true
|
||||
|
||||
serviceAccount:
|
||||
# Specifies whether a ServiceAccount should be created
|
||||
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:
|
||||
|
||||
podServiceAccount:
|
||||
# The name of the ServiceAccount to be used by postgres cluster pods
|
||||
# If not set a name is generated using the fullname template and "-pod" suffix
|
||||
name: "postgres-pod"
|
||||
|
||||
# priority class for operator pod
|
||||
priorityClassName: ""
|
||||
|
||||
# priority class for database pods
|
||||
podPriorityClassName: ""
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 500Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 250Mi
|
||||
|
||||
securityContext:
|
||||
runAsUser: 1000
|
||||
runAsNonRoot: true
|
||||
readOnlyRootFilesystem: true
|
||||
allowPrivilegeEscalation: false
|
||||
|
||||
# Affinity for pod assignment
|
||||
# Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity
|
||||
affinity: {}
|
||||
|
||||
# Node labels for pod assignment
|
||||
# Ref: https://kubernetes.io/docs/user-guide/node-selection/
|
||||
nodeSelector: {}
|
||||
|
||||
# Tolerations for pod assignment
|
||||
# Ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
|
||||
tolerations: []
|
||||
|
||||
controllerID:
|
||||
# Specifies whether a controller ID should be defined for the operator
|
||||
# Note, all postgres manifest must then contain the following annotation to be found by this operator
|
||||
# "acid.zalan.do/controller": <controller-ID-of-the-operator>
|
||||
create: false
|
||||
# The name of the controller ID to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name:
|
||||
|
|
@ -1,19 +1,19 @@
|
|||
image:
|
||||
registry: registry.opensource.zalan.do
|
||||
repository: acid/postgres-operator
|
||||
tag: v1.6.3
|
||||
tag: v1.7.0
|
||||
pullPolicy: "IfNotPresent"
|
||||
|
||||
# Optionally specify an array of imagePullSecrets.
|
||||
# Secrets must be manually created in the namespace.
|
||||
# ref: https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod
|
||||
# imagePullSecrets:
|
||||
# Optionally specify an array of imagePullSecrets.
|
||||
# Secrets must be manually created in the namespace.
|
||||
# ref: https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod
|
||||
# imagePullSecrets:
|
||||
# - name: myRegistryKeySecretName
|
||||
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
|
||||
configTarget: "ConfigMap"
|
||||
configTarget: "OperatorConfigurationCRD"
|
||||
|
||||
# JSON logging format
|
||||
enableJsonLogging: false
|
||||
|
|
@ -21,37 +21,38 @@ enableJsonLogging: false
|
|||
# general configuration parameters
|
||||
configGeneral:
|
||||
# choose if deployment creates/updates CRDs with OpenAPIV3Validation
|
||||
enable_crd_validation: "true"
|
||||
enable_crd_validation: true
|
||||
# update only the statefulsets without immediately doing the rolling update
|
||||
enable_lazy_spilo_upgrade: "false"
|
||||
enable_lazy_spilo_upgrade: false
|
||||
# set the PGVERSION env var instead of providing the version via postgresql.bin_dir in SPILO_CONFIGURATION
|
||||
enable_pgversion_env_var: "true"
|
||||
enable_pgversion_env_var: true
|
||||
# start any new database pod without limitations on shm memory
|
||||
enable_shm_volume: "true"
|
||||
enable_shm_volume: true
|
||||
# enables backwards compatible path between Spilo 12 and Spilo 13 images
|
||||
enable_spilo_wal_path_compat: "false"
|
||||
enable_spilo_wal_path_compat: false
|
||||
# etcd connection string for Patroni. Empty uses K8s-native DCS.
|
||||
etcd_host: ""
|
||||
# Select if setup uses endpoints (default), or configmaps to manage leader (DCS=k8s)
|
||||
# kubernetes_use_configmaps: "false"
|
||||
# kubernetes_use_configmaps: false
|
||||
# Spilo docker image
|
||||
docker_image: registry.opensource.zalan.do/acid/spilo-13:2.0-p7
|
||||
docker_image: registry.opensource.zalan.do/acid/spilo-13:2.1-p1
|
||||
# min number of instances in Postgres cluster. -1 = no limit
|
||||
min_instances: "-1"
|
||||
min_instances: -1
|
||||
# max number of instances in Postgres cluster. -1 = no limit
|
||||
max_instances: "-1"
|
||||
max_instances: -1
|
||||
# period between consecutive repair requests
|
||||
repair_period: 5m
|
||||
# period between consecutive sync requests
|
||||
resync_period: 30m
|
||||
# can prevent certain cases of memory overcommitment
|
||||
# set_memory_request_to_limit: "false"
|
||||
# set_memory_request_to_limit: false
|
||||
|
||||
# map of sidecar names to docker images
|
||||
# sidecar_docker_images: ""
|
||||
# sidecar_docker_images:
|
||||
# example: "exampleimage:exampletag"
|
||||
|
||||
# number of routines the operator spawns to process requests concurrently
|
||||
workers: "8"
|
||||
workers: 8
|
||||
|
||||
# parameters describing Postgres users
|
||||
configUsers:
|
||||
|
|
@ -70,16 +71,20 @@ configMajorVersionUpgrade:
|
|||
|
||||
configKubernetes:
|
||||
# list of additional capabilities for postgres container
|
||||
# additional_pod_capabilities: "SYS_NICE"
|
||||
# additional_pod_capabilities:
|
||||
# - "SYS_NICE"
|
||||
|
||||
# default DNS domain of K8s cluster where operator is running
|
||||
cluster_domain: cluster.local
|
||||
# additional labels assigned to the cluster objects
|
||||
cluster_labels: application:spilo
|
||||
cluster_labels:
|
||||
application: spilo
|
||||
# label assigned to Kubernetes objects created by the operator
|
||||
cluster_name_label: cluster-name
|
||||
# annotations attached to each database pod
|
||||
# custom_pod_annotations: "keya:valuea,keyb:valueb"
|
||||
# additional annotations to add to every database pod
|
||||
# custom_pod_annotations:
|
||||
# keya: valuea
|
||||
# keyb: valueb
|
||||
|
||||
# key name for annotation that compares manifest value with current date
|
||||
# delete_annotation_date_key: "delete-date"
|
||||
|
|
@ -88,30 +93,38 @@ configKubernetes:
|
|||
# delete_annotation_name_key: "delete-clustername"
|
||||
|
||||
# list of annotations propagated from cluster manifest to statefulset and deployment
|
||||
# downscaler_annotations: "deployment-time,downscaler/*"
|
||||
# downscaler_annotations:
|
||||
# - deployment-time
|
||||
# - downscaler/*
|
||||
|
||||
# allow user secrets in other namespaces than the Postgres cluster
|
||||
enable_cross_namespace_secret: false
|
||||
# enables initContainers to run actions before Spilo is started
|
||||
enable_init_containers: "true"
|
||||
enable_init_containers: true
|
||||
# toggles pod anti affinity on the Postgres pods
|
||||
enable_pod_antiaffinity: "false"
|
||||
enable_pod_antiaffinity: false
|
||||
# toggles PDB to set to MinAvailabe 0 or 1
|
||||
enable_pod_disruption_budget: "true"
|
||||
enable_pod_disruption_budget: true
|
||||
# enables sidecar containers to run alongside Spilo in the same pod
|
||||
enable_sidecars: "true"
|
||||
enable_sidecars: true
|
||||
# namespaced name of the secret containing infrastructure roles names and passwords
|
||||
# infrastructure_roles_secret_name: postgresql-infrastructure-roles
|
||||
|
||||
# list of annotation keys that can be inherited from the cluster manifest
|
||||
# inherited_annotations: owned-by
|
||||
# inherited_annotations:
|
||||
# - owned-by
|
||||
|
||||
# list of label keys that can be inherited from the cluster manifest
|
||||
# inherited_labels: application,environment
|
||||
# inherited_labels:
|
||||
# - application
|
||||
# - environment
|
||||
|
||||
# timeout for successful migration of master pods from unschedulable node
|
||||
# master_pod_move_timeout: 20m
|
||||
|
||||
# set of labels that a running and active node should possess to be considered ready
|
||||
# node_readiness_label: ""
|
||||
# node_readiness_label:
|
||||
# status: ready
|
||||
|
||||
# namespaced name of the secret containing the OAuth2 token to pass to the teams API
|
||||
# oauth_token_secret_name: postgresql-operator
|
||||
|
|
@ -137,19 +150,22 @@ configKubernetes:
|
|||
|
||||
# Postgres pods are terminated forcefully after this timeout
|
||||
pod_terminate_grace_period: 5m
|
||||
# template for database user secrets generated by the operator
|
||||
# template for database user secrets generated by the operator,
|
||||
# here username contains the namespace in the format namespace.username
|
||||
# if the user is in different namespace than cluster and cross namespace secrets
|
||||
# are enabled via `enable_cross_namespace_secret` flag in the configuration.
|
||||
secret_name_template: "{username}.{cluster}.credentials.{tprkind}.{tprgroup}"
|
||||
# set user and group for the spilo container (required to run Spilo as non-root process)
|
||||
# spilo_runasuser: "101"
|
||||
# spilo_runasgroup: "103"
|
||||
# spilo_runasuser: 101
|
||||
# spilo_runasgroup: 103
|
||||
# group ID with write-access to volumes (required to run Spilo as non-root process)
|
||||
# spilo_fsgroup: "103"
|
||||
# spilo_fsgroup: 103
|
||||
|
||||
# whether the Spilo container should run in privileged mode
|
||||
spilo_privileged: "false"
|
||||
spilo_privileged: false
|
||||
# whether the Spilo container should run with additional permissions other than parent.
|
||||
# required by cron which needs setuid
|
||||
spilo_allow_privilege_escalation: "true"
|
||||
spilo_allow_privilege_escalation: true
|
||||
# storage resize strategy, available options are: ebs, pvc, off
|
||||
storage_resize_mode: pvc
|
||||
# operator watches for postgres objects in the given namespace
|
||||
|
|
@ -190,34 +206,36 @@ configLoadBalancer:
|
|||
# DNS zone for cluster DNS name when load balancer is configured for cluster
|
||||
db_hosted_zone: db.example.com
|
||||
# annotations to apply to service when load balancing is enabled
|
||||
# custom_service_annotations: "keyx:valuez,keya:valuea"
|
||||
# custom_service_annotations:
|
||||
# keyx: valuez
|
||||
# keya: valuea
|
||||
|
||||
# toggles service type load balancer pointing to the master pod of the cluster
|
||||
enable_master_load_balancer: "false"
|
||||
enable_master_load_balancer: false
|
||||
# toggles service type load balancer pointing to the replica pod of the cluster
|
||||
enable_replica_load_balancer: "false"
|
||||
enable_replica_load_balancer: false
|
||||
# define external traffic policy for the load balancer
|
||||
external_traffic_policy: "Cluster"
|
||||
# defines the DNS name string template for the master load balancer cluster
|
||||
master_dns_name_format: '{cluster}.{team}.{hostedzone}'
|
||||
master_dns_name_format: "{cluster}.{team}.{hostedzone}"
|
||||
# defines the DNS name string template for the replica load balancer cluster
|
||||
replica_dns_name_format: '{cluster}-repl.{team}.{hostedzone}'
|
||||
replica_dns_name_format: "{cluster}-repl.{team}.{hostedzone}"
|
||||
|
||||
# options to aid debugging of the operator itself
|
||||
configDebug:
|
||||
# toggles verbose debug logs from the operator
|
||||
debug_logging: "true"
|
||||
debug_logging: true
|
||||
# toggles operator functionality that require access to the postgres database
|
||||
enable_database_access: "true"
|
||||
enable_database_access: true
|
||||
|
||||
# parameters affecting logging and REST API listener
|
||||
configLoggingRestApi:
|
||||
# REST API listener listens to this port
|
||||
api_port: "8080"
|
||||
api_port: 8080
|
||||
# number of entries in the cluster history ring buffer
|
||||
cluster_history_entries: "1000"
|
||||
cluster_history_entries: 1000
|
||||
# number of lines in the ring buffer used to store cluster logs
|
||||
ring_log_lines: "100"
|
||||
ring_log_lines: 100
|
||||
|
||||
# configure interaction with non-Kubernetes objects from AWS or GCP
|
||||
configAwsOrGcp:
|
||||
|
|
@ -231,11 +249,11 @@ configAwsOrGcp:
|
|||
aws_region: eu-central-1
|
||||
|
||||
# enable automatic migration on AWS from gp2 to gp3 volumes
|
||||
enable_ebs_gp3_migration: "false"
|
||||
enable_ebs_gp3_migration: false
|
||||
# defines maximum volume size in GB until which auto migration happens
|
||||
# enable_ebs_gp3_migration_max_size: "1000"
|
||||
# enable_ebs_gp3_migration_max_size: 1000
|
||||
|
||||
# GCP credentials for setting the GOOGLE_APPLICATION_CREDNETIALS environment variable
|
||||
# GCP credentials that will be used by the operator / pods
|
||||
# gcp_credentials: ""
|
||||
|
||||
# AWS IAM role to supply in the iam.amazonaws.com/role annotation of Postgres pods
|
||||
|
|
@ -250,10 +268,13 @@ configAwsOrGcp:
|
|||
# GCS bucket to use for shipping WAL segments with WAL-E
|
||||
# wal_gs_bucket: ""
|
||||
|
||||
# Azure Storage Account to use for shipping WAL segments with WAL-G
|
||||
# wal_az_storage_account: ""
|
||||
|
||||
# configure K8s cron job managed by the operator
|
||||
configLogicalBackup:
|
||||
# image for pods of the logical backup job (example runs pg_dumpall)
|
||||
logical_backup_docker_image: "registry.opensource.zalan.do/acid/logical-backup:v1.6.3"
|
||||
logical_backup_docker_image: "registry.opensource.zalan.do/acid/logical-backup:v1.7.0"
|
||||
# path of google cloud service account json file
|
||||
# logical_backup_google_application_credentials: ""
|
||||
|
||||
|
|
@ -265,10 +286,10 @@ configLogicalBackup:
|
|||
logical_backup_s3_access_key_id: ""
|
||||
# S3 bucket to store backup results
|
||||
logical_backup_s3_bucket: "my-bucket-url"
|
||||
# S3 endpoint url when not using AWS
|
||||
logical_backup_s3_endpoint: ""
|
||||
# S3 region of bucket
|
||||
logical_backup_s3_region: ""
|
||||
# S3 endpoint url when not using AWS
|
||||
logical_backup_s3_endpoint: ""
|
||||
# S3 Secret Access Key
|
||||
logical_backup_s3_secret_access_key: ""
|
||||
# S3 server side encryption
|
||||
|
|
@ -276,36 +297,38 @@ configLogicalBackup:
|
|||
# backup schedule in the cron format
|
||||
logical_backup_schedule: "30 00 * * *"
|
||||
|
||||
|
||||
# automate creation of human users with teams API service
|
||||
configTeamsApi:
|
||||
# team_admin_role will have the rights to grant roles coming from PG manifests
|
||||
enable_admin_role_for_users: "true"
|
||||
enable_admin_role_for_users: true
|
||||
# operator watches for PostgresTeam CRs to assign additional teams and members to clusters
|
||||
enable_postgres_team_crd: "false"
|
||||
enable_postgres_team_crd: false
|
||||
# toogle to create additional superuser teams from PostgresTeam CRs
|
||||
enable_postgres_team_crd_superusers: "false"
|
||||
enable_postgres_team_crd_superusers: false
|
||||
# toggle to automatically rename roles of former team members and deny LOGIN
|
||||
enable_team_member_deprecation: "false"
|
||||
enable_team_member_deprecation: false
|
||||
# toggle to grant superuser to team members created from the Teams API
|
||||
enable_team_superuser: "false"
|
||||
enable_team_superuser: false
|
||||
# toggles usage of the Teams API by the operator
|
||||
enable_teams_api: "false"
|
||||
enable_teams_api: false
|
||||
# should contain a URL to use for authentication (username and token)
|
||||
# pam_configuration: https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees
|
||||
|
||||
# operator will add all team member roles to this group and add a pg_hba line
|
||||
pam_role_name: "zalandos"
|
||||
pam_role_name: zalandos
|
||||
# List of teams which members need the superuser role in each Postgres cluster
|
||||
postgres_superuser_teams: "postgres_superusers"
|
||||
postgres_superuser_teams:
|
||||
- postgres_superusers
|
||||
# List of roles that cannot be overwritten by an application, team or infrastructure role
|
||||
protected_role_names: "admin"
|
||||
protected_role_names:
|
||||
- admin
|
||||
# Suffix to add if members are removed from TeamsAPI or PostgresTeam CRD
|
||||
role_deletion_suffix: "_deleted"
|
||||
# role name to grant to team members created from the Teams API
|
||||
team_admin_role: "admin"
|
||||
team_admin_role: admin
|
||||
# postgres config parameters to apply to each team member role
|
||||
team_api_role_configuration: "log_statement:all"
|
||||
team_api_role_configuration:
|
||||
log_statement: all
|
||||
# URL of the Teams API service
|
||||
# teams_api_url: http://fake-teams-api.default.svc.cluster.local
|
||||
|
||||
|
|
@ -316,13 +339,13 @@ configConnectionPooler:
|
|||
# db user for pooler to use
|
||||
connection_pooler_user: "pooler"
|
||||
# docker image
|
||||
connection_pooler_image: "registry.opensource.zalan.do/acid/pgbouncer:master-16"
|
||||
connection_pooler_image: "registry.opensource.zalan.do/acid/pgbouncer:master-18"
|
||||
# max db connections the pooler should hold
|
||||
connection_pooler_max_db_connections: "60"
|
||||
connection_pooler_max_db_connections: 60
|
||||
# default pooling mode
|
||||
connection_pooler_mode: "transaction"
|
||||
# number of pooler instances
|
||||
connection_pooler_number_of_instances: "2"
|
||||
connection_pooler_number_of_instances: 2
|
||||
# default resources
|
||||
connection_pooler_default_cpu_request: 500m
|
||||
connection_pooler_default_memory_request: 100Mi
|
||||
|
|
|
|||
|
|
@ -3,6 +3,21 @@
|
|||
Learn how to configure and manage the Postgres Operator in your Kubernetes (K8s)
|
||||
environment.
|
||||
|
||||
## Upgrading the operator
|
||||
|
||||
The Postgres Operator is upgraded by changing the docker image within the
|
||||
deployment. Before doing so, it is recommended to check the release notes
|
||||
for new configuration options or changed behavior you might want to reflect
|
||||
in the ConfigMap or config CRD. E.g. a new feature might get introduced which
|
||||
is enabled or disabled by default and you want to change it to the opposite
|
||||
with the corresponding flag option.
|
||||
|
||||
When using helm, be aware that installing the new chart will not update the
|
||||
`Postgresql` and `OperatorConfiguration` CRD. Make sure to update them before
|
||||
with the provided manifests in the `crds` folder. Otherwise, you might face
|
||||
errors about new Postgres manifest or configuration options being unknown
|
||||
to the CRD schema validation.
|
||||
|
||||
## Minor and major version upgrade
|
||||
|
||||
Minor version upgrades for PostgreSQL are handled via updating the Spilo Docker
|
||||
|
|
@ -157,16 +172,26 @@ from numerous escape characters in the latter log entry, view it in CLI with
|
|||
`PodTemplate` used by the operator is yet to be updated with the default values
|
||||
used internally in K8s.
|
||||
|
||||
The operator also support lazy updates of the Spilo image. That means the pod
|
||||
template of a PG cluster's stateful set is updated immediately with the new
|
||||
image, but no rolling update follows. This feature saves you a switchover - and
|
||||
hence downtime - when you know pods are re-started later anyway, for instance
|
||||
due to the node rotation. To force a rolling update, disable this mode by
|
||||
setting the `enable_lazy_spilo_upgrade` to `false` in the operator configuration
|
||||
and restart the operator pod. With the standard eager rolling updates the
|
||||
operator checks during Sync all pods run images specified in their respective
|
||||
statefulsets. The operator triggers a rolling upgrade for PG clusters that
|
||||
violate this condition.
|
||||
The StatefulSet is replaced if the following properties change:
|
||||
- annotations
|
||||
- volumeClaimTemplates
|
||||
- template volumes
|
||||
|
||||
The StatefulSet is replaced and a rolling updates is triggered if the following
|
||||
properties differ between the old and new state:
|
||||
- container name, ports, image, resources, env, envFrom, securityContext and volumeMounts
|
||||
- template labels, annotations, service account, securityContext, affinity, priority class and termination grace period
|
||||
|
||||
Note that, changes in `SPILO_CONFIGURATION` env variable under `bootstrap.dcs`
|
||||
path are ignored for the diff. They will be applied through Patroni's rest api
|
||||
interface, following a restart of all instances.
|
||||
|
||||
The operator also support lazy updates of the Spilo image. In this case the
|
||||
StatefulSet is only updated, but no rolling update follows. This feature saves
|
||||
you a switchover - and hence downtime - when you know pods are re-started later
|
||||
anyway, for instance due to the node rotation. To force a rolling update,
|
||||
disable this mode by setting the `enable_lazy_spilo_upgrade` to `false` in the
|
||||
operator configuration and restart the operator pod.
|
||||
|
||||
## Delete protection via annotations
|
||||
|
||||
|
|
@ -663,6 +688,12 @@ if it ends up in your specified WAL backup path:
|
|||
envdir "/run/etc/wal-e.d/env" /scripts/postgres_backup.sh "/home/postgres/pgdata/pgroot/data"
|
||||
```
|
||||
|
||||
You can also check if Spilo is able to find any backups:
|
||||
|
||||
```bash
|
||||
envdir "/run/etc/wal-e.d/env" wal-g backup-list
|
||||
```
|
||||
|
||||
Depending on the cloud storage provider different [environment variables](https://github.com/zalando/spilo/blob/master/ENVIRONMENT.rst)
|
||||
have to be set for Spilo. Not all of them are generated automatically by the
|
||||
operator by changing its configuration. In this case you have to use an
|
||||
|
|
@ -730,8 +761,15 @@ WALE_S3_ENDPOINT='https+path://s3.eu-central-1.amazonaws.com:443'
|
|||
WALE_S3_PREFIX=$WAL_S3_BUCKET/spilo/{WAL_BUCKET_SCOPE_PREFIX}{SCOPE}{WAL_BUCKET_SCOPE_SUFFIX}/wal/{PGVERSION}
|
||||
```
|
||||
|
||||
If the prefix is not specified Spilo will generate it from `WAL_S3_BUCKET`.
|
||||
When the `AWS_REGION` is set `AWS_ENDPOINT` and `WALE_S3_ENDPOINT` are
|
||||
The operator sets the prefix to an empty string so that spilo will generate it
|
||||
from the configured `WAL_S3_BUCKET`.
|
||||
|
||||
:warning: When you overwrite the configuration by defining `WAL_S3_BUCKET` in
|
||||
the [pod_environment_configmap](#custom-pod-environment-variables) you have
|
||||
to set `WAL_BUCKET_SCOPE_PREFIX = ""`, too. Otherwise Spilo will not find
|
||||
the physical backups on restore (next chapter).
|
||||
|
||||
When the `AWS_REGION` is set, `AWS_ENDPOINT` and `WALE_S3_ENDPOINT` are
|
||||
generated automatically. `WALG_S3_PREFIX` is identical to `WALE_S3_PREFIX`.
|
||||
`SCOPE` is the Postgres cluster name.
|
||||
|
||||
|
|
@ -804,6 +842,63 @@ pod_environment_configmap: "postgres-operator-system/pod-env-overrides"
|
|||
...
|
||||
```
|
||||
|
||||
### Azure setup
|
||||
|
||||
To configure the operator on Azure these prerequisites are needed:
|
||||
|
||||
* A storage account in the same region as the Kubernetes cluster.
|
||||
|
||||
The configuration parameters that we will be using are:
|
||||
|
||||
* `pod_environment_secret`
|
||||
* `wal_az_storage_account`
|
||||
|
||||
1. Generate the K8s secret resource that will contain your storage account's
|
||||
access key. You will need a copy of this secret in every namespace you want to
|
||||
create postgresql clusters.
|
||||
|
||||
The latest version of WAL-G (v1.0) supports the use of a SASS token, but you'll
|
||||
have to make due with using the primary or secondary access token until the
|
||||
version of WAL-G is updated in the postgres-operator.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: psql-backup-creds
|
||||
namespace: default
|
||||
type: Opaque
|
||||
stringData:
|
||||
AZURE_STORAGE_ACCESS_KEY: <primary or secondary access key>
|
||||
```
|
||||
|
||||
2. Setup pod environment configmap that instructs the operator to use WAL-G,
|
||||
instead of WAL-E, for backup and restore.
|
||||
```yml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: pod-env-overrides
|
||||
namespace: postgres-operator-system
|
||||
data:
|
||||
# Any env variable used by spilo can be added
|
||||
USE_WALG_BACKUP: "true"
|
||||
USE_WALG_RESTORE: "true"
|
||||
CLONE_USE_WALG_RESTORE: "true"
|
||||
```
|
||||
|
||||
3. Setup your operator configuration values. With the `psql-backup-creds`
|
||||
and `pod-env-overrides` resources applied to your cluster, ensure that the operator's configuration
|
||||
is set up like the following:
|
||||
```yml
|
||||
...
|
||||
aws_or_gcp:
|
||||
pod_environment_secret: "pgsql-backup-creds"
|
||||
pod_environment_configmap: "postgres-operator-system/pod-env-overrides"
|
||||
wal_az_storage_account: "postgresbackupsbucket28302F2" # name of storage account to save the WAL-G logs
|
||||
...
|
||||
```
|
||||
|
||||
### Restoring physical backups
|
||||
|
||||
If cluster members have to be (re)initialized restoring physical backups
|
||||
|
|
@ -813,6 +908,36 @@ on one of the other running instances (preferably replicas if they do not lag
|
|||
behind). You can test restoring backups by [cloning](user.md#how-to-clone-an-existing-postgresql-cluster)
|
||||
clusters.
|
||||
|
||||
If you need to provide a [custom clone environment](#custom-pod-environment-variables)
|
||||
copy existing variables about your setup (backup location, prefix, access
|
||||
keys etc.) and prepend the `CLONE_` prefix to get them copied to the correct
|
||||
directory within Spilo.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: postgres-pod-config
|
||||
data:
|
||||
AWS_REGION: "eu-west-1"
|
||||
AWS_ACCESS_KEY_ID: "****"
|
||||
AWS_SECRET_ACCESS_KEY: "****"
|
||||
...
|
||||
CLONE_AWS_REGION: "eu-west-1"
|
||||
CLONE_AWS_ACCESS_KEY_ID: "****"
|
||||
CLONE_AWS_SECRET_ACCESS_KEY: "****"
|
||||
...
|
||||
```
|
||||
|
||||
### Standby clusters
|
||||
|
||||
The setup for [standby clusters](user.md#setting-up-a-standby-cluster) is very
|
||||
similar to cloning. At the moment, the operator only allows for streaming from
|
||||
the S3 WAL archive of the master specified in the manifest. Like with cloning,
|
||||
if you are using [additional environment variables](#custom-pod-environment-variables)
|
||||
to access your backup location you have to copy those variables and prepend the
|
||||
`STANDBY_` prefix for Spilo to find the backups and WAL files to stream.
|
||||
|
||||
## Logical backups
|
||||
|
||||
The operator can manage K8s cron jobs to run logical backups (SQL dumps) of
|
||||
|
|
@ -950,7 +1075,7 @@ make docker
|
|||
|
||||
# build in image in minikube docker env
|
||||
eval $(minikube docker-env)
|
||||
docker build -t registry.opensource.zalan.do/acid/postgres-operator-ui:v1.6.3 .
|
||||
docker build -t registry.opensource.zalan.do/acid/postgres-operator-ui:v1.7.0 .
|
||||
|
||||
# apply UI manifests next to a running Postgres Operator
|
||||
kubectl apply -f manifests/
|
||||
|
|
|
|||
|
|
@ -314,13 +314,12 @@ Please, reflect your changes in tests, for example in:
|
|||
|
||||
For the CRD-based configuration, please update the following files:
|
||||
* the default [OperatorConfiguration](../manifests/postgresql-operator-default-configuration.yaml)
|
||||
* the Helm chart's [values-crd file](../charts/postgres-operator/values.yaml)
|
||||
* the CRD's [validation](../manifests/operatorconfiguration.crd.yaml)
|
||||
* the CRD's validation in the [Helm chart](../charts/postgres-operator/crds/operatorconfigurations.yaml)
|
||||
|
||||
Reflect the changes in the ConfigMap configuration as well (note that numeric
|
||||
and boolean parameters have to use double quotes here):
|
||||
* [ConfigMap](../manifests/configmap.yaml) manifest
|
||||
* the Helm chart's default [values file](../charts/postgres-operator/values.yaml)
|
||||
Add new options also to the Helm chart's [values file](../charts/postgres-operator/values.yaml) file.
|
||||
It follows the OperatorConfiguration CRD layout. Nested values will be flattened for the ConfigMap.
|
||||
Last but no least, update the [ConfigMap](../manifests/configmap.yaml) manifest example as well.
|
||||
|
||||
### Updating documentation
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ solutions:
|
|||
* [kind](https://kind.sigs.k8s.io/) and [k3d](https://k3d.io), which allows creating multi-nodes K8s
|
||||
clusters running on Docker (requires Docker)
|
||||
|
||||
To interact with the K8s infrastructure install it's CLI runtime [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-via-curl).
|
||||
To interact with the K8s infrastructure install its CLI runtime [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-via-curl).
|
||||
|
||||
This quickstart assumes that you have started minikube or created a local kind
|
||||
cluster. Note that you can also use built-in K8s support in the Docker Desktop
|
||||
|
|
@ -81,16 +81,12 @@ the repo root. With Helm v3 installed you should be able to run:
|
|||
helm install postgres-operator ./charts/postgres-operator
|
||||
```
|
||||
|
||||
To use CRD-based configuration you need to specify the [values-crd yaml file](../charts/postgres-operator/values-crd.yaml).
|
||||
|
||||
```bash
|
||||
helm install postgres-operator ./charts/postgres-operator -f ./charts/postgres-operator/values-crd.yaml
|
||||
```
|
||||
|
||||
The chart works with both Helm 2 and Helm 3. The `crd-install` hook from v2 will
|
||||
be skipped with warning when using v3. Documentation for installing applications
|
||||
with Helm 2 can be found in the [v2 docs](https://v2.helm.sh/docs/).
|
||||
|
||||
The chart is also hosted at: https://opensource.zalando.com/postgres-operator/charts/postgres-operator/
|
||||
|
||||
## Check if Postgres Operator is running
|
||||
|
||||
Starting the operator may take a few seconds. Check if the operator pod is
|
||||
|
|
|
|||
|
|
@ -109,7 +109,11 @@ These parameters are grouped directly under the `spec` key in the manifest.
|
|||
`SUPERUSER`, `REPLICATION`, `INHERIT`, `LOGIN`, `NOLOGIN`, `CREATEROLE`,
|
||||
`CREATEDB`, `BYPASSURL`. A login user is created by default unless NOLOGIN is
|
||||
specified, in which case the operator creates a role. One can specify empty
|
||||
flags by providing a JSON empty array '*[]*'. Optional.
|
||||
flags by providing a JSON empty array '*[]*'. If the config option
|
||||
`enable_cross_namespace_secrets` is enabled you can specify the namespace in
|
||||
the user name in the form `{namespace}.{username}` and the operator will
|
||||
create the K8s secret in that namespace. The part after the first `.` is
|
||||
considered to be the user name. Optional.
|
||||
|
||||
* **databases**
|
||||
a map of database names to database owners for the databases that should be
|
||||
|
|
@ -185,6 +189,35 @@ These parameters are grouped directly under the `spec` key in the manifest.
|
|||
If you set the `all` special item, it will be mounted in all containers (postgres + sidecars).
|
||||
Else you can set the list of target containers in which the additional volumes will be mounted (eg : postgres, telegraf)
|
||||
|
||||
## Prepared Databases
|
||||
|
||||
The operator can create databases with default owner, reader and writer roles
|
||||
without the need to specifiy them under `users` or `databases` sections. Those
|
||||
parameters are grouped under the `preparedDatabases` top-level key. For more
|
||||
information, see [user docs](../user.md#prepared-databases-with-roles-and-default-privileges).
|
||||
|
||||
* **defaultUsers**
|
||||
The operator will always create default `NOLOGIN` roles for defined prepared
|
||||
databases, but if `defaultUsers` is set to `true` three additional `LOGIN`
|
||||
roles with `_user` suffix will get created. Default is `false`.
|
||||
|
||||
* **extensions**
|
||||
map of extensions with target database schema that the operator will install
|
||||
in the database. Optional.
|
||||
|
||||
* **schemas**
|
||||
map of schemas that the operator will create. Optional - if no schema is
|
||||
listed, the operator will create a schema called `data`. Under each schema
|
||||
key, it can be defined if `defaultRoles` (NOLOGIN) and `defaultUsers` (LOGIN)
|
||||
roles shall be created that have schema-exclusive privileges. Both flags are
|
||||
set to `false` by default.
|
||||
|
||||
* **secretNamespace**
|
||||
for each default LOGIN role the operator will create a secret. You can
|
||||
specify the namespace in which these secrets will get created, if
|
||||
`enable_cross_namespace_secrets` is set to `true` in the config. Otherwise,
|
||||
the cluster namespace is used.
|
||||
|
||||
## Postgres parameters
|
||||
|
||||
Those parameters are grouped under the `postgresql` top-level key, which is
|
||||
|
|
@ -258,7 +291,9 @@ explanation of `ttl` and `loop_wait` parameters.
|
|||
|
||||
Those parameters define [CPU and memory requests and limits](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/)
|
||||
for the Postgres container. They are grouped under the `resources` top-level
|
||||
key with subgroups `requests` and `limits`.
|
||||
key with subgroups `requests` and `limits`. The whole section is optional,
|
||||
however if you specify a request or limit you have to define everything
|
||||
(unless you are not modifying the default CRD schema validation).
|
||||
|
||||
### Requests
|
||||
|
||||
|
|
@ -266,11 +301,11 @@ CPU and memory requests for the Postgres container.
|
|||
|
||||
* **cpu**
|
||||
CPU requests for the Postgres container. Optional, overrides the
|
||||
`default_cpu_requests` operator configuration parameter. Optional.
|
||||
`default_cpu_requests` operator configuration parameter.
|
||||
|
||||
* **memory**
|
||||
memory requests for the Postgres container. Optional, overrides the
|
||||
`default_memory_request` operator configuration parameter. Optional.
|
||||
`default_memory_request` operator configuration parameter.
|
||||
|
||||
### Limits
|
||||
|
||||
|
|
@ -278,11 +313,11 @@ CPU and memory limits for the Postgres container.
|
|||
|
||||
* **cpu**
|
||||
CPU limits for the Postgres container. Optional, overrides the
|
||||
`default_cpu_limits` operator configuration parameter. Optional.
|
||||
`default_cpu_limits` operator configuration parameter.
|
||||
|
||||
* **memory**
|
||||
memory limits for the Postgres container. Optional, overrides the
|
||||
`default_memory_limits` operator configuration parameter. Optional.
|
||||
`default_memory_limits` operator configuration parameter.
|
||||
|
||||
## Parameters defining how to clone the cluster from another one
|
||||
|
||||
|
|
@ -364,6 +399,11 @@ properties of the persistent storage that stores Postgres data.
|
|||
When running the operator on AWS the latest generation of EBS volumes (`gp3`)
|
||||
allows for configuring the throughput in MB/s. Maximum is 1000. Optional.
|
||||
|
||||
* **selector**
|
||||
A label query over PVs to consider for binding. See the [Kubernetes
|
||||
documentation](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/)
|
||||
for details on using `matchLabels` and `matchExpressions`. Optional
|
||||
|
||||
## Sidecar definitions
|
||||
|
||||
Those parameters are defined under the `sidecars` key. They consist of a list
|
||||
|
|
|
|||
|
|
@ -172,11 +172,11 @@ under the `users` key.
|
|||
|
||||
## Major version upgrades
|
||||
|
||||
Parameters configuring automatic major version upgrades. In a
|
||||
Parameters configuring automatic major version upgrades. In a
|
||||
CRD-configuration, they are grouped under the `major_version_upgrade` key.
|
||||
|
||||
* **major_version_upgrade_mode**
|
||||
Postgres Operator supports [in-place major version upgrade](../administrator.md#in-place-major-version-upgrade)
|
||||
Postgres Operator supports [in-place major version upgrade](../administrator.md#in-place-major-version-upgrade)
|
||||
with three different modes:
|
||||
`"off"` = no upgrade by the operator,
|
||||
`"manual"` = manifest triggers action,
|
||||
|
|
@ -264,6 +264,11 @@ configuration they are grouped under the `kubernetes` key.
|
|||
[admin docs](../administrator.md#pod-disruption-budget) for more information.
|
||||
Default is true.
|
||||
|
||||
* **enable_cross_namespace_secrets**
|
||||
To allow secrets in a different namespace other than the Postgres cluster
|
||||
namespace. Once enabled, specify the namespace in the user name under the
|
||||
`users` section in the form `{namespace}.{username}`. The default is `false`.
|
||||
|
||||
* **enable_init_containers**
|
||||
global option to allow for creating init containers in the cluster manifest to
|
||||
run actions before Spilo is started. Default is true.
|
||||
|
|
@ -275,11 +280,13 @@ configuration they are grouped under the `kubernetes` key.
|
|||
|
||||
* **secret_name_template**
|
||||
a template for the name of the database user secrets generated by the
|
||||
operator. `{username}` is replaced with name of the secret, `{cluster}` with
|
||||
the name of the cluster, `{tprkind}` with the kind of CRD (formerly known as
|
||||
TPR) and `{tprgroup}` with the group of the CRD. No other placeholders are
|
||||
allowed. The default is
|
||||
`{username}.{cluster}.credentials.{tprkind}.{tprgroup}`.
|
||||
operator. `{namespace}` is replaced with name of the namespace if
|
||||
`enable_cross_namespace_secret` is set, otherwise the
|
||||
secret is in cluster's namespace. `{username}` is replaced with name of the
|
||||
secret, `{cluster}` with the name of the cluster, `{tprkind}` with the kind
|
||||
of CRD (formerly known as TPR) and `{tprgroup}` with the group of the CRD.
|
||||
No other placeholders are allowed. The default is
|
||||
`{namespace}.{username}.{cluster}.credentials.{tprkind}.{tprgroup}`.
|
||||
|
||||
* **cluster_domain**
|
||||
defines the default DNS domain for the kubernetes cluster the operator is
|
||||
|
|
@ -550,6 +557,12 @@ yet officially supported.
|
|||
[service accounts](https://cloud.google.com/kubernetes-engine/docs/tutorials/authenticating-to-cloud-platform).
|
||||
The default is empty
|
||||
|
||||
* **wal_az_storage_account**
|
||||
Azure Storage Account to use for shipping WAL segments with WAL-G. The
|
||||
storage account must exist and be accessible by Postgres pods. Note, only the
|
||||
name of the storage account is required.
|
||||
The default is empty.
|
||||
|
||||
* **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
|
||||
|
|
@ -593,7 +606,7 @@ grouped under the `logical_backup` key.
|
|||
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:v1.6.3"
|
||||
pipeline. Default: "registry.opensource.zalan.do/acid/logical-backup:v1.7.0"
|
||||
|
||||
* **logical_backup_google_application_credentials**
|
||||
Specifies the path of the google cloud service account json file. Default is empty.
|
||||
|
|
|
|||
55
docs/user.md
55
docs/user.md
|
|
@ -139,6 +139,26 @@ secret, without ever sharing it outside of the cluster.
|
|||
At the moment it is not possible to define membership of the manifest role in
|
||||
other roles.
|
||||
|
||||
To define the secrets for the users in a different namespace than that of the
|
||||
cluster, one can set `enable_cross_namespace_secret` and declare the namespace
|
||||
for the secrets in the manifest in the following manner,
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
users:
|
||||
#users with secret in dfferent namespace
|
||||
appspace.db_user:
|
||||
- createdb
|
||||
```
|
||||
|
||||
Here, anything before the first dot is considered the namespace and the text after
|
||||
the first dot is the username. Also, the postgres roles of these usernames would
|
||||
be in the form of `namespace.username`.
|
||||
|
||||
For such usernames, the secret is created in the given namespace and its name is
|
||||
of the following form,
|
||||
`{namespace}.{username}.{team}-{clustername}.credentials.postgresql.acid.zalan.do`
|
||||
|
||||
### Infrastructure roles
|
||||
|
||||
An infrastructure role is a role that should be present on every PostgreSQL
|
||||
|
|
@ -330,7 +350,7 @@ spec:
|
|||
|
||||
This creates roles for members of the `c-team` team not only in all clusters
|
||||
owned by `a-team`, but as well in cluster owned by `b-team`, as `a-team` is
|
||||
an `additionalTeam` to `b-team`
|
||||
an `additionalTeam` to `b-team`
|
||||
|
||||
Not, you can also define `additionalSuperuserTeams` in the `PostgresTeam`
|
||||
manifest. By default, this option is disabled and must be configured with
|
||||
|
|
@ -501,9 +521,10 @@ Then, the schemas are owned by the database owner, too.
|
|||
|
||||
The roles described in the previous paragraph can be granted to LOGIN roles from
|
||||
the `users` section in the manifest. Optionally, the Postgres Operator can also
|
||||
create default LOGIN roles for the database an each schema individually. These
|
||||
create default LOGIN roles for the database and each schema individually. These
|
||||
roles will get the `_user` suffix and they inherit all rights from their NOLOGIN
|
||||
counterparts.
|
||||
counterparts. Therefore, you cannot have `defaultRoles` set to `false` and enable
|
||||
`defaultUsers` at the same time.
|
||||
|
||||
| Role name | Member of | Admin |
|
||||
| ------------------- | -------------- | ------------- |
|
||||
|
|
@ -526,6 +547,23 @@ spec:
|
|||
defaultUsers: true
|
||||
```
|
||||
|
||||
Default access privileges are also defined for LOGIN roles on database and
|
||||
schema creation. This means they are currently not set when `defaultUsers`
|
||||
(or `defaultRoles` for schemas) are enabled at a later point in time.
|
||||
|
||||
For all LOGIN roles the operator will create K8s secrets in the namespace
|
||||
specified in `secretNamespace`, if `enable_cross_namespace_secret` is set to
|
||||
`true` in the config. Otherwise, they are created in the same namespace like
|
||||
the Postgres cluster.
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
preparedDatabases:
|
||||
foo:
|
||||
defaultUsers: true
|
||||
secretNamespace: appspace
|
||||
```
|
||||
|
||||
### Schema `search_path` for default roles
|
||||
|
||||
The schema [`search_path`](https://www.postgresql.org/docs/13/ddl-schemas.html#DDL-SCHEMAS-PATH)
|
||||
|
|
@ -695,20 +733,21 @@ spec:
|
|||
uid: "efd12e58-5786-11e8-b5a7-06148230260c"
|
||||
cluster: "acid-batman"
|
||||
timestamp: "2017-12-19T12:40:33+01:00"
|
||||
s3_wal_path: "s3://<bucketname>/spilo/<source_db_cluster>/<UID>/wal/<PGVERSION>"
|
||||
```
|
||||
|
||||
Here `cluster` is a name of a source cluster that is going to be cloned. A new
|
||||
cluster will be cloned from S3, using the latest backup before the `timestamp`.
|
||||
Note, that a time zone is required for `timestamp` in the format of +00:00 which
|
||||
is UTC. The `uid` field is also mandatory. The operator will use it to find a
|
||||
correct key inside an S3 bucket. You can find this field in the metadata of the
|
||||
source cluster:
|
||||
is UTC. You can specify the `s3_wal_path` of the source cluster or let the
|
||||
operator try to find it based on the configured `wal_[s3|gs]_bucket` and the
|
||||
specified `uid`. You can find the UID of the source cluster in its metadata:
|
||||
|
||||
```yaml
|
||||
apiVersion: acid.zalan.do/v1
|
||||
kind: postgresql
|
||||
metadata:
|
||||
name: acid-test-cluster
|
||||
name: acid-batman
|
||||
uid: efd12e58-5786-11e8-b5a7-06148230260c
|
||||
```
|
||||
|
||||
|
|
@ -761,7 +800,7 @@ no statefulset will be created.
|
|||
```yaml
|
||||
spec:
|
||||
standby:
|
||||
s3_wal_path: "s3 bucket path to the master"
|
||||
s3_wal_path: "s3://<bucketname>/spilo/<source_db_cluster>/<UID>/wal/<PGVERSION>"
|
||||
```
|
||||
|
||||
At the moment, the operator only allows to stream from the WAL archive of the
|
||||
|
|
|
|||
|
|
@ -156,6 +156,10 @@ class K8s:
|
|||
while not get_services():
|
||||
time.sleep(self.RETRY_TIMEOUT_SEC)
|
||||
|
||||
def count_pods_with_rolling_update_flag(self, labels, namespace='default'):
|
||||
pods = self.api.core_v1.list_namespaced_pod(namespace, label_selector=labels).items
|
||||
return len(list(filter(lambda x: "zalando-postgres-operator-rolling-update-required" in x.metadata.annotations, pods)))
|
||||
|
||||
def count_pods_with_label(self, labels, namespace='default'):
|
||||
return len(self.api.core_v1.list_namespaced_pod(namespace, label_selector=labels).items)
|
||||
|
||||
|
|
@ -189,6 +193,7 @@ class K8s:
|
|||
def wait_for_pod_failover(self, failover_targets, labels, namespace='default'):
|
||||
pod_phase = 'Failing over'
|
||||
new_pod_node = ''
|
||||
pods_with_update_flag = self.count_pods_with_rolling_update_flag(labels, namespace)
|
||||
|
||||
while (pod_phase != 'Running') or (new_pod_node not in failover_targets):
|
||||
pods = self.api.core_v1.list_namespaced_pod(namespace, label_selector=labels).items
|
||||
|
|
@ -196,6 +201,20 @@ class K8s:
|
|||
new_pod_node = pods[0].spec.node_name
|
||||
pod_phase = pods[0].status.phase
|
||||
time.sleep(self.RETRY_TIMEOUT_SEC)
|
||||
|
||||
while pods_with_update_flag != 0:
|
||||
pods_with_update_flag = self.count_pods_with_rolling_update_flag(labels, namespace)
|
||||
time.sleep(self.RETRY_TIMEOUT_SEC)
|
||||
|
||||
def wait_for_namespace_creation(self, namespace='default'):
|
||||
ns_found = False
|
||||
while ns_found != True:
|
||||
ns = self.api.core_v1.list_namespace().items
|
||||
for n in ns:
|
||||
if n.metadata.name == namespace:
|
||||
ns_found = True
|
||||
break
|
||||
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")
|
||||
|
|
@ -233,6 +252,13 @@ class K8s:
|
|||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
|
||||
def patroni_rest(self, pod, path):
|
||||
r = self.exec_with_kubectl(pod, "curl localhost:8008/" + path)
|
||||
if not r.returncode == 0 or not r.stdout.decode()[0:1] == "{":
|
||||
return None
|
||||
|
||||
return json.loads(r.stdout.decode())
|
||||
|
||||
def get_patroni_state(self, pod):
|
||||
r = self.exec_with_kubectl(pod, "patronictl list -f json")
|
||||
if not r.returncode == 0 or not r.stdout.decode()[0:1] == "[":
|
||||
|
|
@ -413,6 +439,10 @@ class K8sBase:
|
|||
while not get_services():
|
||||
time.sleep(self.RETRY_TIMEOUT_SEC)
|
||||
|
||||
def count_pods_with_rolling_update_flag(self, labels, namespace='default'):
|
||||
pods = self.api.core_v1.list_namespaced_pod(namespace, label_selector=labels).items
|
||||
return len(list(filter(lambda x: "zalando-postgres-operator-rolling-update-required" in x.metadata.annotations, pods)))
|
||||
|
||||
def count_pods_with_label(self, labels, namespace='default'):
|
||||
return len(self.api.core_v1.list_namespaced_pod(namespace, label_selector=labels).items)
|
||||
|
||||
|
|
@ -446,6 +476,7 @@ class K8sBase:
|
|||
def wait_for_pod_failover(self, failover_targets, labels, namespace='default'):
|
||||
pod_phase = 'Failing over'
|
||||
new_pod_node = ''
|
||||
pods_with_update_flag = self.count_pods_with_rolling_update_flag(labels, namespace)
|
||||
|
||||
while (pod_phase != 'Running') or (new_pod_node not in failover_targets):
|
||||
pods = self.api.core_v1.list_namespaced_pod(namespace, label_selector=labels).items
|
||||
|
|
@ -454,6 +485,10 @@ class K8sBase:
|
|||
pod_phase = pods[0].status.phase
|
||||
time.sleep(self.RETRY_TIMEOUT_SEC)
|
||||
|
||||
while pods_with_update_flag != 0:
|
||||
pods_with_update_flag = self.count_pods_with_rolling_update_flag(labels, namespace)
|
||||
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")
|
||||
|
||||
|
|
@ -486,6 +521,13 @@ class K8sBase:
|
|||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
|
||||
def patroni_rest(self, pod, path):
|
||||
r = self.exec_with_kubectl(pod, "curl localhost:8008/" + path)
|
||||
if not r.returncode == 0 or not r.stdout.decode()[0:1] == "{":
|
||||
return None
|
||||
|
||||
return json.loads(r.stdout.decode())
|
||||
|
||||
def get_patroni_state(self, pod):
|
||||
r = self.exec_with_kubectl(pod, "patronictl list -f json")
|
||||
if not r.returncode == 0 or not r.stdout.decode()[0:1] == "[":
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ class EndToEndTestCase(unittest.TestCase):
|
|||
WHERE (rolname = 'tester' AND rolcanlogin)
|
||||
OR (rolname = 'kind_delete_me' AND NOT rolcanlogin);
|
||||
"""
|
||||
self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", user_query)), 2,
|
||||
self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", user_query)), 2,
|
||||
"Database role of replaced member in PostgresTeam not renamed", 10, 5)
|
||||
|
||||
# re-add additional member and check if the role is renamed back
|
||||
|
|
@ -276,7 +276,7 @@ class EndToEndTestCase(unittest.TestCase):
|
|||
WHERE (rolname = 'kind' AND rolcanlogin)
|
||||
OR (rolname = 'tester_delete_me' AND NOT rolcanlogin);
|
||||
"""
|
||||
self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", user_query)), 2,
|
||||
self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", user_query)), 2,
|
||||
"Database role of recreated member in PostgresTeam not renamed back to original name", 10, 5)
|
||||
|
||||
# revert config change
|
||||
|
|
@ -290,38 +290,39 @@ class EndToEndTestCase(unittest.TestCase):
|
|||
"Operator does not get in sync")
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_overwrite_pooler_deployment(self):
|
||||
self.k8s.create_with_kubectl("manifests/minimal-fake-pooler-deployment.yaml")
|
||||
self.eventuallyEqual(lambda: self.k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||
self.eventuallyEqual(lambda: self.k8s.get_deployment_replica_count(name="acid-minimal-cluster-pooler"), 1,
|
||||
"Initial broken deployment not rolled out")
|
||||
def test_cross_namespace_secrets(self):
|
||||
'''
|
||||
Test secrets in different namespace
|
||||
'''
|
||||
k8s = self.k8s
|
||||
|
||||
self.k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||
'acid.zalan.do', 'v1', 'default',
|
||||
'postgresqls', 'acid-minimal-cluster',
|
||||
{
|
||||
'spec': {
|
||||
'enableConnectionPooler': True
|
||||
# enable secret creation in separate namespace
|
||||
patch_cross_namespace_secret = {
|
||||
"data": {
|
||||
"enable_cross_namespace_secret": "true"
|
||||
}
|
||||
})
|
||||
|
||||
self.eventuallyEqual(lambda: self.k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||
self.eventuallyEqual(lambda: self.k8s.get_deployment_replica_count(name="acid-minimal-cluster-pooler"), 2,
|
||||
"Operator did not succeed in overwriting labels")
|
||||
}
|
||||
self.k8s.update_config(patch_cross_namespace_secret,
|
||||
step="cross namespace secrets enabled")
|
||||
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"},
|
||||
"Operator does not get in sync")
|
||||
|
||||
# create secret in test namespace
|
||||
self.k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||
'acid.zalan.do', 'v1', 'default',
|
||||
'postgresqls', 'acid-minimal-cluster',
|
||||
{
|
||||
'spec': {
|
||||
'enableConnectionPooler': False
|
||||
}
|
||||
})
|
||||
|
||||
self.eventuallyEqual(lambda: self.k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||
self.eventuallyEqual(lambda: self.k8s.count_running_pods("connection-pooler=acid-minimal-cluster-pooler"),
|
||||
0, "Pooler pods not scaled down")
|
||||
|
||||
'acid.zalan.do', 'v1', 'default',
|
||||
'postgresqls', 'acid-minimal-cluster',
|
||||
{
|
||||
'spec': {
|
||||
'users':{
|
||||
'test.db_user': [],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"},
|
||||
"Operator does not get in sync")
|
||||
self.eventuallyEqual(lambda: self.k8s.count_secrets_with_label("cluster-name=acid-minimal-cluster,application=spilo", self.test_namespace),
|
||||
1, "Secret not created for user in namespace")
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_enable_disable_connection_pooler(self):
|
||||
|
|
@ -568,6 +569,7 @@ class EndToEndTestCase(unittest.TestCase):
|
|||
role.pop("Password", None)
|
||||
self.assertDictEqual(role, {
|
||||
"Name": "robot_zmon_acid_monitoring_new",
|
||||
"Namespace":"",
|
||||
"Flags": None,
|
||||
"MemberOf": ["robot_zmon"],
|
||||
"Parameters": None,
|
||||
|
|
@ -753,6 +755,33 @@ class EndToEndTestCase(unittest.TestCase):
|
|||
# ensure cluster is healthy after tests
|
||||
self.eventuallyEqual(lambda: len(k8s.get_patroni_running_members("acid-minimal-cluster-0")), 2, "Postgres status did not enter running")
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
@unittest.skip("Skipping this test until fixed")
|
||||
def test_major_version_upgrade(self):
|
||||
k8s = self.k8s
|
||||
result = k8s.create_with_kubectl("manifests/minimal-postgres-manifest-12.yaml")
|
||||
self.eventuallyEqual(lambda: k8s.count_running_pods(labels="application=spilo,cluster-name=acid-upgrade-test"), 2, "No 2 pods running")
|
||||
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||
|
||||
pg_patch_version = {
|
||||
"spec": {
|
||||
"postgres": {
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
}
|
||||
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||
"acid.zalan.do", "v1", "default", "postgresqls", "acid-upgrade-test", pg_patch_version)
|
||||
|
||||
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||
|
||||
def check_version_13():
|
||||
p = k8s.get_patroni_state("acid-upgrade-test-0")
|
||||
version = p["server_version"][0:2]
|
||||
return version
|
||||
|
||||
self.evantuallyEqual(check_version_13, "13", "Version was not upgrade to 13")
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_min_resource_limits(self):
|
||||
'''
|
||||
|
|
@ -823,6 +852,7 @@ class EndToEndTestCase(unittest.TestCase):
|
|||
try:
|
||||
k8s.create_with_kubectl("manifests/complete-postgres-manifest.yaml")
|
||||
k8s.wait_for_pod_start("spilo-role=master", self.test_namespace)
|
||||
k8s.wait_for_pod_start("spilo-role=replica", self.test_namespace)
|
||||
self.assert_master_is_unique(self.test_namespace, "acid-test-cluster")
|
||||
|
||||
except timeout_decorator.TimeoutError:
|
||||
|
|
@ -836,360 +866,6 @@ class EndToEndTestCase(unittest.TestCase):
|
|||
"acid.zalan.do", "v1", self.test_namespace, "postgresqls", "acid-test-cluster")
|
||||
time.sleep(5)
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_rolling_update_flag(self):
|
||||
'''
|
||||
Add rolling update flag to only the master and see it failing over
|
||||
'''
|
||||
k8s = self.k8s
|
||||
cluster_label = 'application=spilo,cluster-name=acid-minimal-cluster'
|
||||
|
||||
# verify we are in good state from potential previous tests
|
||||
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2, "No 2 pods running")
|
||||
|
||||
# get node and replica (expected target of new master)
|
||||
_, replica_nodes = k8s.get_pg_nodes(cluster_label)
|
||||
|
||||
# rolling update annotation
|
||||
flag = {
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"zalando-postgres-operator-rolling-update-required": "true",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
podsList = k8s.api.core_v1.list_namespaced_pod('default', label_selector=cluster_label)
|
||||
for pod in podsList.items:
|
||||
# add flag only to the master to make it appear to the operator as a leftover from a rolling update
|
||||
if pod.metadata.labels.get('spilo-role') == 'master':
|
||||
old_creation_timestamp = pod.metadata.creation_timestamp
|
||||
k8s.patch_pod(flag, pod.metadata.name, pod.metadata.namespace)
|
||||
else:
|
||||
# remember replica name to check if operator does a switchover
|
||||
switchover_target = pod.metadata.name
|
||||
|
||||
# do not wait until the next sync
|
||||
k8s.delete_operator_pod()
|
||||
|
||||
# operator should now recreate the master pod and do a switchover before
|
||||
k8s.wait_for_pod_failover(replica_nodes, 'spilo-role=master,' + cluster_label)
|
||||
|
||||
# check if the former replica is now the new master
|
||||
leader = k8s.get_cluster_leader_pod()
|
||||
self.eventuallyEqual(lambda: leader.metadata.name, switchover_target, "Rolling update flag did not trigger switchover")
|
||||
|
||||
# check that the old master has been recreated
|
||||
k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label)
|
||||
replica = k8s.get_cluster_replica_pod()
|
||||
self.assertTrue(replica.metadata.creation_timestamp > old_creation_timestamp, "Old master pod was not recreated")
|
||||
|
||||
|
||||
except timeout_decorator.TimeoutError:
|
||||
print('Operator log: {}'.format(k8s.get_operator_log()))
|
||||
raise
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_rolling_update_label_timeout(self):
|
||||
'''
|
||||
Simulate case when replica does not receive label in time and rolling update does not finish
|
||||
'''
|
||||
k8s = self.k8s
|
||||
cluster_label = 'application=spilo,cluster-name=acid-minimal-cluster'
|
||||
flag = "zalando-postgres-operator-rolling-update-required"
|
||||
|
||||
# verify we are in good state from potential previous tests
|
||||
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2, "No 2 pods running")
|
||||
|
||||
# get node and replica (expected target of new master)
|
||||
_, replica_nodes = k8s.get_pg_nodes(cluster_label)
|
||||
|
||||
# rolling update annotation
|
||||
rolling_update_patch = {
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
flag: "true",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# make pod_label_wait_timeout so short that rolling update fails on first try
|
||||
# temporarily lower resync interval to reduce waiting for further tests
|
||||
# pods should get healthy in the meantime
|
||||
patch_resync_config = {
|
||||
"data": {
|
||||
"pod_label_wait_timeout": "2s",
|
||||
"resync_period": "20s",
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
# patch both pods for rolling update
|
||||
podList = k8s.api.core_v1.list_namespaced_pod('default', label_selector=cluster_label)
|
||||
for pod in podList.items:
|
||||
k8s.patch_pod(rolling_update_patch, pod.metadata.name, pod.metadata.namespace)
|
||||
if pod.metadata.labels.get('spilo-role') == 'replica':
|
||||
switchover_target = pod.metadata.name
|
||||
|
||||
# update config and restart operator
|
||||
k8s.update_config(patch_resync_config, "update resync interval and pod_label_wait_timeout")
|
||||
|
||||
# operator should now recreate the replica pod first and do a switchover after
|
||||
k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label)
|
||||
|
||||
# pod_label_wait_timeout should have been exceeded hence the rolling update is continued on next sync
|
||||
# check if the cluster state is "SyncFailed"
|
||||
self.eventuallyEqual(lambda: k8s.pg_get_status(), "SyncFailed", "Expected SYNC event to fail")
|
||||
|
||||
# wait for next sync, replica should be running normally by now and be ready for switchover
|
||||
k8s.wait_for_pod_failover(replica_nodes, 'spilo-role=master,' + cluster_label)
|
||||
|
||||
# check if the former replica is now the new master
|
||||
leader = k8s.get_cluster_leader_pod()
|
||||
self.eventuallyEqual(lambda: leader.metadata.name, switchover_target, "Rolling update flag did not trigger switchover")
|
||||
|
||||
# wait for the old master to get restarted
|
||||
k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label)
|
||||
|
||||
# status should again be "SyncFailed" but turn into "Running" on the next sync
|
||||
time.sleep(10)
|
||||
self.eventuallyEqual(lambda: k8s.pg_get_status(), "Running", "Expected running cluster after two syncs")
|
||||
|
||||
# revert config changes
|
||||
patch_resync_config = {
|
||||
"data": {
|
||||
"pod_label_wait_timeout": "10m",
|
||||
"resync_period": "30m",
|
||||
}
|
||||
}
|
||||
k8s.update_config(patch_resync_config, "revert resync interval and pod_label_wait_timeout")
|
||||
|
||||
|
||||
except timeout_decorator.TimeoutError:
|
||||
print('Operator log: {}'.format(k8s.get_operator_log()))
|
||||
raise
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_zz_node_readiness_label(self):
|
||||
'''
|
||||
Remove node readiness label from master node. This must cause a failover.
|
||||
'''
|
||||
k8s = self.k8s
|
||||
cluster_label = 'application=spilo,cluster-name=acid-minimal-cluster'
|
||||
readiness_label = 'lifecycle-status'
|
||||
readiness_value = 'ready'
|
||||
|
||||
try:
|
||||
# get nodes of master and replica(s) (expected target of new master)
|
||||
current_master_node, current_replica_nodes = k8s.get_pg_nodes(cluster_label)
|
||||
num_replicas = len(current_replica_nodes)
|
||||
failover_targets = self.get_failover_targets(current_master_node, current_replica_nodes)
|
||||
|
||||
# add node_readiness_label to potential failover nodes
|
||||
patch_readiness_label = {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
readiness_label: readiness_value
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertTrue(len(failover_targets) > 0, "No failover targets available")
|
||||
for failover_target in failover_targets:
|
||||
k8s.api.core_v1.patch_node(failover_target, patch_readiness_label)
|
||||
|
||||
# define node_readiness_label in config map which should trigger a failover of the master
|
||||
patch_readiness_label_config = {
|
||||
"data": {
|
||||
"node_readiness_label": readiness_label + ':' + readiness_value,
|
||||
}
|
||||
}
|
||||
k8s.update_config(patch_readiness_label_config, "setting readiness label")
|
||||
new_master_node, new_replica_nodes = self.assert_failover(
|
||||
current_master_node, num_replicas, failover_targets, cluster_label)
|
||||
|
||||
# patch also node where master ran before
|
||||
k8s.api.core_v1.patch_node(current_master_node, patch_readiness_label)
|
||||
|
||||
# toggle pod anti affinity to move replica away from master node
|
||||
self.eventuallyTrue(lambda: self.assert_distributed_pods(new_master_node, new_replica_nodes, cluster_label), "Pods are redistributed")
|
||||
|
||||
except timeout_decorator.TimeoutError:
|
||||
print('Operator log: {}'.format(k8s.get_operator_log()))
|
||||
raise
|
||||
|
||||
@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
|
||||
pod = "acid-minimal-cluster-0"
|
||||
|
||||
k8s.scale_cluster(3)
|
||||
self.eventuallyEqual(lambda: k8s.count_running_pods(), 3, "Scale up to 3 failed")
|
||||
self.eventuallyEqual(lambda: len(k8s.get_patroni_running_members(pod)), 3, "Not all 3 nodes healthy")
|
||||
|
||||
k8s.scale_cluster(2)
|
||||
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2, "Scale down to 2 failed")
|
||||
self.eventuallyEqual(lambda: len(k8s.get_patroni_running_members(pod)), 2, "Not all members 2 healthy")
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_service_annotations(self):
|
||||
'''
|
||||
Create a Postgres cluster with service annotations and check them.
|
||||
'''
|
||||
k8s = self.k8s
|
||||
patch_custom_service_annotations = {
|
||||
"data": {
|
||||
"custom_service_annotations": "foo:bar",
|
||||
}
|
||||
}
|
||||
k8s.update_config(patch_custom_service_annotations)
|
||||
|
||||
pg_patch_custom_annotations = {
|
||||
"spec": {
|
||||
"serviceAnnotations": {
|
||||
"annotation.key": "value",
|
||||
"alice": "bob",
|
||||
}
|
||||
}
|
||||
}
|
||||
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_custom_annotations)
|
||||
|
||||
annotations = {
|
||||
"annotation.key": "value",
|
||||
"foo": "bar",
|
||||
"alice": "bob"
|
||||
}
|
||||
|
||||
self.eventuallyTrue(lambda: k8s.check_service_annotations("cluster-name=acid-minimal-cluster,spilo-role=master", annotations), "Wrong annotations")
|
||||
self.eventuallyTrue(lambda: k8s.check_service_annotations("cluster-name=acid-minimal-cluster,spilo-role=replica", annotations), "Wrong annotations")
|
||||
|
||||
# clean up
|
||||
unpatch_custom_service_annotations = {
|
||||
"data": {
|
||||
"custom_service_annotations": "",
|
||||
}
|
||||
}
|
||||
k8s.update_config(unpatch_custom_service_annotations)
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_statefulset_annotation_propagation(self):
|
||||
'''
|
||||
Inject annotation to Postgresql CRD and check it's propagation to stateful set
|
||||
'''
|
||||
k8s = self.k8s
|
||||
cluster_label = 'application=spilo,cluster-name=acid-minimal-cluster'
|
||||
|
||||
patch_sset_propagate_annotations = {
|
||||
"data": {
|
||||
"downscaler_annotations": "deployment-time,downscaler/*",
|
||||
"inherited_annotations": "owned-by",
|
||||
}
|
||||
}
|
||||
k8s.update_config(patch_sset_propagate_annotations)
|
||||
|
||||
pg_crd_annotations = {
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"deployment-time": "2020-04-30 12:00:00",
|
||||
"downscaler/downtime_replicas": "0",
|
||||
"owned-by": "acid",
|
||||
},
|
||||
}
|
||||
}
|
||||
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_crd_annotations)
|
||||
|
||||
annotations = {
|
||||
"deployment-time": "2020-04-30 12:00:00",
|
||||
"downscaler/downtime_replicas": "0",
|
||||
"owned-by": "acid",
|
||||
}
|
||||
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||
self.eventuallyTrue(lambda: k8s.check_statefulset_annotations(cluster_label, annotations), "Annotations missing")
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
@unittest.skip("Skipping this test until fixed")
|
||||
def test_zaa_test_major_version_upgrade(self):
|
||||
k8s = self.k8s
|
||||
result = k8s.create_with_kubectl("manifests/minimal-postgres-manifest-12.yaml")
|
||||
self.eventuallyEqual(lambda: k8s.count_running_pods(labels="application=spilo,cluster-name=acid-upgrade-test"), 2, "No 2 pods running")
|
||||
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||
|
||||
pg_patch_version = {
|
||||
"spec": {
|
||||
"postgres": {
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
}
|
||||
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||
"acid.zalan.do", "v1", "default", "postgresqls", "acid-upgrade-test", pg_patch_version)
|
||||
|
||||
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||
|
||||
def check_version_13():
|
||||
p = k8s.get_patroni_state("acid-upgrade-test-0")
|
||||
version = p["server_version"][0:2]
|
||||
return version
|
||||
|
||||
self.evantuallyEqual(check_version_13, "13", "Version was not upgrade to 13")
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
@unittest.skip("Skipping this test until fixed")
|
||||
def test_zzz_taint_based_eviction(self):
|
||||
'''
|
||||
Add taint "postgres=:NoExecute" to node with master. This must cause a failover.
|
||||
'''
|
||||
k8s = self.k8s
|
||||
cluster_label = 'application=spilo,cluster-name=acid-minimal-cluster'
|
||||
|
||||
# verify we are in good state from potential previous tests
|
||||
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2, "No 2 pods running")
|
||||
self.eventuallyEqual(lambda: len(k8s.get_patroni_running_members("acid-minimal-cluster-0")), 2, "Postgres status did not enter running")
|
||||
|
||||
# get nodes of master and replica(s) (expected target of new master)
|
||||
master_nodes, replica_nodes = k8s.get_cluster_nodes()
|
||||
|
||||
self.assertNotEqual(master_nodes, [])
|
||||
self.assertNotEqual(replica_nodes, [])
|
||||
|
||||
# taint node with postgres=:NoExecute to force failover
|
||||
body = {
|
||||
"spec": {
|
||||
"taints": [
|
||||
{
|
||||
"effect": "NoExecute",
|
||||
"key": "postgres"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
k8s.api.core_v1.patch_node(master_nodes[0], body)
|
||||
self.eventuallyTrue(lambda: k8s.get_cluster_nodes()[0], replica_nodes)
|
||||
self.assertNotEqual(lambda: k8s.get_cluster_nodes()[0], master_nodes)
|
||||
|
||||
# add toleration to pods
|
||||
patch_toleration_config = {
|
||||
"data": {
|
||||
"toleration": "key:postgres,operator:Exists,effect:NoExecute"
|
||||
}
|
||||
}
|
||||
|
||||
k8s.update_config(patch_toleration_config, step="allow tainted nodes")
|
||||
|
||||
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2, "No 2 pods running")
|
||||
self.eventuallyEqual(lambda: len(k8s.get_patroni_running_members("acid-minimal-cluster-0")), 2, "Postgres status did not enter running")
|
||||
|
||||
# toggle pod anti affinity to move replica away from master node
|
||||
nm, new_replica_nodes = k8s.get_cluster_nodes()
|
||||
new_master_node = nm[0]
|
||||
self.assert_distributed_pods(new_master_node, new_replica_nodes, cluster_label)
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_node_affinity(self):
|
||||
'''
|
||||
|
|
@ -1299,7 +975,449 @@ class EndToEndTestCase(unittest.TestCase):
|
|||
raise
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_zzzz_cluster_deletion(self):
|
||||
def test_node_readiness_label(self):
|
||||
'''
|
||||
Remove node readiness label from master node. This must cause a failover.
|
||||
'''
|
||||
k8s = self.k8s
|
||||
cluster_label = 'application=spilo,cluster-name=acid-minimal-cluster'
|
||||
readiness_label = 'lifecycle-status'
|
||||
readiness_value = 'ready'
|
||||
|
||||
try:
|
||||
# get nodes of master and replica(s) (expected target of new master)
|
||||
current_master_node, current_replica_nodes = k8s.get_pg_nodes(cluster_label)
|
||||
num_replicas = len(current_replica_nodes)
|
||||
failover_targets = self.get_failover_targets(current_master_node, current_replica_nodes)
|
||||
|
||||
# add node_readiness_label to potential failover nodes
|
||||
patch_readiness_label = {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
readiness_label: readiness_value
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertTrue(len(failover_targets) > 0, "No failover targets available")
|
||||
for failover_target in failover_targets:
|
||||
k8s.api.core_v1.patch_node(failover_target, patch_readiness_label)
|
||||
|
||||
# define node_readiness_label in config map which should trigger a failover of the master
|
||||
patch_readiness_label_config = {
|
||||
"data": {
|
||||
"node_readiness_label": readiness_label + ':' + readiness_value,
|
||||
}
|
||||
}
|
||||
k8s.update_config(patch_readiness_label_config, "setting readiness label")
|
||||
new_master_node, new_replica_nodes = self.assert_failover(
|
||||
current_master_node, num_replicas, failover_targets, cluster_label)
|
||||
|
||||
# patch also node where master ran before
|
||||
k8s.api.core_v1.patch_node(current_master_node, patch_readiness_label)
|
||||
|
||||
# toggle pod anti affinity to move replica away from master node
|
||||
self.eventuallyTrue(lambda: self.assert_distributed_pods(new_master_node, new_replica_nodes, cluster_label), "Pods are redistributed")
|
||||
|
||||
except timeout_decorator.TimeoutError:
|
||||
print('Operator log: {}'.format(k8s.get_operator_log()))
|
||||
raise
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_overwrite_pooler_deployment(self):
|
||||
self.k8s.create_with_kubectl("manifests/minimal-fake-pooler-deployment.yaml")
|
||||
self.eventuallyEqual(lambda: self.k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||
self.eventuallyEqual(lambda: self.k8s.get_deployment_replica_count(name="acid-minimal-cluster-pooler"), 1,
|
||||
"Initial broken deployment not rolled out")
|
||||
|
||||
self.k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||
'acid.zalan.do', 'v1', 'default',
|
||||
'postgresqls', 'acid-minimal-cluster',
|
||||
{
|
||||
'spec': {
|
||||
'enableConnectionPooler': True
|
||||
}
|
||||
})
|
||||
|
||||
self.eventuallyEqual(lambda: self.k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||
self.eventuallyEqual(lambda: self.k8s.get_deployment_replica_count(name="acid-minimal-cluster-pooler"), 2,
|
||||
"Operator did not succeed in overwriting labels")
|
||||
|
||||
self.k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||
'acid.zalan.do', 'v1', 'default',
|
||||
'postgresqls', 'acid-minimal-cluster',
|
||||
{
|
||||
'spec': {
|
||||
'enableConnectionPooler': False
|
||||
}
|
||||
})
|
||||
|
||||
self.eventuallyEqual(lambda: self.k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||
self.eventuallyEqual(lambda: self.k8s.count_running_pods("connection-pooler=acid-minimal-cluster-pooler"),
|
||||
0, "Pooler pods not scaled down")
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_patroni_config_update(self):
|
||||
'''
|
||||
Change Postgres config under Spec.Postgresql.Parameters and Spec.Patroni
|
||||
and query Patroni config endpoint to check if manifest changes got applied
|
||||
via restarting cluster through Patroni's rest api
|
||||
'''
|
||||
k8s = self.k8s
|
||||
masterPod = k8s.get_cluster_leader_pod()
|
||||
labels = 'application=spilo,cluster-name=acid-minimal-cluster,spilo-role=master'
|
||||
creationTimestamp = masterPod.metadata.creation_timestamp
|
||||
new_max_connections_value = "50"
|
||||
|
||||
# adjust max_connection
|
||||
pg_patch_config = {
|
||||
"spec": {
|
||||
"postgresql": {
|
||||
"parameters": {
|
||||
"max_connections": new_max_connections_value
|
||||
}
|
||||
},
|
||||
"patroni": {
|
||||
"slots": {
|
||||
"test_slot": {
|
||||
"type": "physical"
|
||||
}
|
||||
},
|
||||
"ttl": 29,
|
||||
"loop_wait": 9,
|
||||
"retry_timeout": 9,
|
||||
"synchronous_mode": True
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_config)
|
||||
|
||||
self.eventuallyEqual(lambda: self.k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||
|
||||
def compare_config():
|
||||
effective_config = k8s.patroni_rest(masterPod.metadata.name, "config")
|
||||
desired_patroni = pg_patch_config["spec"]["patroni"]
|
||||
desired_parameters = pg_patch_config["spec"]["postgresql"]["parameters"]
|
||||
effective_parameters = effective_config["postgresql"]["parameters"]
|
||||
self.assertEqual(desired_parameters["max_connections"], effective_parameters["max_connections"],
|
||||
"max_connections not updated")
|
||||
self.assertTrue(effective_config["slots"] is not None, "physical replication slot not added")
|
||||
self.assertEqual(desired_patroni["ttl"], effective_config["ttl"],
|
||||
"ttl not updated")
|
||||
self.assertEqual(desired_patroni["loop_wait"], effective_config["loop_wait"],
|
||||
"loop_wait not updated")
|
||||
self.assertEqual(desired_patroni["retry_timeout"], effective_config["retry_timeout"],
|
||||
"retry_timeout not updated")
|
||||
self.assertEqual(desired_patroni["synchronous_mode"], effective_config["synchronous_mode"],
|
||||
"synchronous_mode not updated")
|
||||
return True
|
||||
|
||||
self.eventuallyTrue(compare_config, "Postgres config not applied")
|
||||
|
||||
setting_query = """
|
||||
SELECT setting
|
||||
FROM pg_settings
|
||||
WHERE name = 'max_connections';
|
||||
"""
|
||||
self.eventuallyEqual(lambda: self.query_database(masterPod.metadata.name, "postgres", setting_query)[0], new_max_connections_value,
|
||||
"New max_connections setting not applied", 10, 5)
|
||||
|
||||
# make sure that pod wasn't recreated
|
||||
self.assertEqual(creationTimestamp, masterPod.metadata.creation_timestamp,
|
||||
"Master pod creation timestamp is updated")
|
||||
|
||||
except timeout_decorator.TimeoutError:
|
||||
print('Operator log: {}'.format(k8s.get_operator_log()))
|
||||
raise
|
||||
|
||||
# make sure cluster is in a good state for further tests
|
||||
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2,
|
||||
"No 2 pods running")
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_rolling_update_flag(self):
|
||||
'''
|
||||
Add rolling update flag to only the master and see it failing over
|
||||
'''
|
||||
k8s = self.k8s
|
||||
cluster_label = 'application=spilo,cluster-name=acid-minimal-cluster'
|
||||
|
||||
# verify we are in good state from potential previous tests
|
||||
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2, "No 2 pods running")
|
||||
|
||||
# get node and replica (expected target of new master)
|
||||
_, replica_nodes = k8s.get_pg_nodes(cluster_label)
|
||||
|
||||
# rolling update annotation
|
||||
flag = {
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"zalando-postgres-operator-rolling-update-required": "true",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
podsList = k8s.api.core_v1.list_namespaced_pod('default', label_selector=cluster_label)
|
||||
for pod in podsList.items:
|
||||
# add flag only to the master to make it appear to the operator as a leftover from a rolling update
|
||||
if pod.metadata.labels.get('spilo-role') == 'master':
|
||||
old_creation_timestamp = pod.metadata.creation_timestamp
|
||||
k8s.patch_pod(flag, pod.metadata.name, pod.metadata.namespace)
|
||||
else:
|
||||
# remember replica name to check if operator does a switchover
|
||||
switchover_target = pod.metadata.name
|
||||
|
||||
# do not wait until the next sync
|
||||
k8s.delete_operator_pod()
|
||||
|
||||
# operator should now recreate the master pod and do a switchover before
|
||||
k8s.wait_for_pod_failover(replica_nodes, 'spilo-role=master,' + cluster_label)
|
||||
|
||||
# check if the former replica is now the new master
|
||||
leader = k8s.get_cluster_leader_pod()
|
||||
self.eventuallyEqual(lambda: leader.metadata.name, switchover_target, "Rolling update flag did not trigger switchover")
|
||||
|
||||
# check that the old master has been recreated
|
||||
k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label)
|
||||
replica = k8s.get_cluster_replica_pod()
|
||||
self.assertTrue(replica.metadata.creation_timestamp > old_creation_timestamp, "Old master pod was not recreated")
|
||||
|
||||
|
||||
except timeout_decorator.TimeoutError:
|
||||
print('Operator log: {}'.format(k8s.get_operator_log()))
|
||||
raise
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_rolling_update_label_timeout(self):
|
||||
'''
|
||||
Simulate case when replica does not receive label in time and rolling update does not finish
|
||||
'''
|
||||
k8s = self.k8s
|
||||
cluster_label = 'application=spilo,cluster-name=acid-minimal-cluster'
|
||||
flag = "zalando-postgres-operator-rolling-update-required"
|
||||
|
||||
# verify we are in good state from potential previous tests
|
||||
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2, "No 2 pods running")
|
||||
|
||||
# get node and replica (expected target of new master)
|
||||
_, replica_nodes = k8s.get_pg_nodes(cluster_label)
|
||||
|
||||
# rolling update annotation
|
||||
rolling_update_patch = {
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
flag: "true",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# make pod_label_wait_timeout so short that rolling update fails on first try
|
||||
# temporarily lower resync interval to reduce waiting for further tests
|
||||
# pods should get healthy in the meantime
|
||||
patch_resync_config = {
|
||||
"data": {
|
||||
"pod_label_wait_timeout": "2s",
|
||||
"resync_period": "30s",
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
# patch both pods for rolling update
|
||||
podList = k8s.api.core_v1.list_namespaced_pod('default', label_selector=cluster_label)
|
||||
for pod in podList.items:
|
||||
k8s.patch_pod(rolling_update_patch, pod.metadata.name, pod.metadata.namespace)
|
||||
if pod.metadata.labels.get('spilo-role') == 'replica':
|
||||
switchover_target = pod.metadata.name
|
||||
|
||||
# update config and restart operator
|
||||
k8s.update_config(patch_resync_config, "update resync interval and pod_label_wait_timeout")
|
||||
|
||||
# operator should now recreate the replica pod first and do a switchover after
|
||||
k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label)
|
||||
|
||||
# pod_label_wait_timeout should have been exceeded hence the rolling update is continued on next sync
|
||||
# check if the cluster state is "SyncFailed"
|
||||
self.eventuallyEqual(lambda: k8s.pg_get_status(), "SyncFailed", "Expected SYNC event to fail")
|
||||
|
||||
# wait for next sync, replica should be running normally by now and be ready for switchover
|
||||
k8s.wait_for_pod_failover(replica_nodes, 'spilo-role=master,' + cluster_label)
|
||||
|
||||
# check if the former replica is now the new master
|
||||
leader = k8s.get_cluster_leader_pod()
|
||||
self.eventuallyEqual(lambda: leader.metadata.name, switchover_target, "Rolling update flag did not trigger switchover")
|
||||
|
||||
# wait for the old master to get restarted
|
||||
k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label)
|
||||
|
||||
# status should again be "SyncFailed" but turn into "Running" on the next sync
|
||||
time.sleep(30)
|
||||
self.eventuallyEqual(lambda: k8s.pg_get_status(), "Running", "Expected running cluster after two syncs")
|
||||
|
||||
# revert config changes
|
||||
patch_resync_config = {
|
||||
"data": {
|
||||
"pod_label_wait_timeout": "10m",
|
||||
"resync_period": "30m",
|
||||
}
|
||||
}
|
||||
k8s.update_config(patch_resync_config, "revert resync interval and pod_label_wait_timeout")
|
||||
|
||||
|
||||
except timeout_decorator.TimeoutError:
|
||||
print('Operator log: {}'.format(k8s.get_operator_log()))
|
||||
raise
|
||||
|
||||
@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
|
||||
pod = "acid-minimal-cluster-0"
|
||||
|
||||
k8s.scale_cluster(3)
|
||||
self.eventuallyEqual(lambda: k8s.count_running_pods(), 3, "Scale up to 3 failed")
|
||||
self.eventuallyEqual(lambda: len(k8s.get_patroni_running_members(pod)), 3, "Not all 3 nodes healthy")
|
||||
|
||||
k8s.scale_cluster(2)
|
||||
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2, "Scale down to 2 failed")
|
||||
self.eventuallyEqual(lambda: len(k8s.get_patroni_running_members(pod)), 2, "Not all members 2 healthy")
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_service_annotations(self):
|
||||
'''
|
||||
Create a Postgres cluster with service annotations and check them.
|
||||
'''
|
||||
k8s = self.k8s
|
||||
patch_custom_service_annotations = {
|
||||
"data": {
|
||||
"custom_service_annotations": "foo:bar",
|
||||
}
|
||||
}
|
||||
k8s.update_config(patch_custom_service_annotations)
|
||||
|
||||
pg_patch_custom_annotations = {
|
||||
"spec": {
|
||||
"serviceAnnotations": {
|
||||
"annotation.key": "value",
|
||||
"alice": "bob",
|
||||
}
|
||||
}
|
||||
}
|
||||
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_custom_annotations)
|
||||
|
||||
annotations = {
|
||||
"annotation.key": "value",
|
||||
"foo": "bar",
|
||||
"alice": "bob"
|
||||
}
|
||||
|
||||
self.eventuallyTrue(lambda: k8s.check_service_annotations("cluster-name=acid-minimal-cluster,spilo-role=master", annotations), "Wrong annotations")
|
||||
self.eventuallyTrue(lambda: k8s.check_service_annotations("cluster-name=acid-minimal-cluster,spilo-role=replica", annotations), "Wrong annotations")
|
||||
|
||||
# clean up
|
||||
unpatch_custom_service_annotations = {
|
||||
"data": {
|
||||
"custom_service_annotations": "",
|
||||
}
|
||||
}
|
||||
k8s.update_config(unpatch_custom_service_annotations)
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_statefulset_annotation_propagation(self):
|
||||
'''
|
||||
Inject annotation to Postgresql CRD and check it's propagation to stateful set
|
||||
'''
|
||||
k8s = self.k8s
|
||||
cluster_label = 'application=spilo,cluster-name=acid-minimal-cluster'
|
||||
|
||||
patch_sset_propagate_annotations = {
|
||||
"data": {
|
||||
"downscaler_annotations": "deployment-time,downscaler/*",
|
||||
"inherited_annotations": "owned-by",
|
||||
}
|
||||
}
|
||||
k8s.update_config(patch_sset_propagate_annotations)
|
||||
|
||||
pg_crd_annotations = {
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"deployment-time": "2020-04-30 12:00:00",
|
||||
"downscaler/downtime_replicas": "0",
|
||||
"owned-by": "acid",
|
||||
},
|
||||
}
|
||||
}
|
||||
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_crd_annotations)
|
||||
|
||||
annotations = {
|
||||
"deployment-time": "2020-04-30 12:00:00",
|
||||
"downscaler/downtime_replicas": "0",
|
||||
"owned-by": "acid",
|
||||
}
|
||||
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||
self.eventuallyTrue(lambda: k8s.check_statefulset_annotations(cluster_label, annotations), "Annotations missing")
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
@unittest.skip("Skipping this test until fixed")
|
||||
def test_taint_based_eviction(self):
|
||||
'''
|
||||
Add taint "postgres=:NoExecute" to node with master. This must cause a failover.
|
||||
'''
|
||||
k8s = self.k8s
|
||||
cluster_label = 'application=spilo,cluster-name=acid-minimal-cluster'
|
||||
|
||||
# verify we are in good state from potential previous tests
|
||||
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2, "No 2 pods running")
|
||||
self.eventuallyEqual(lambda: len(k8s.get_patroni_running_members("acid-minimal-cluster-0")), 2, "Postgres status did not enter running")
|
||||
|
||||
# get nodes of master and replica(s) (expected target of new master)
|
||||
master_nodes, replica_nodes = k8s.get_cluster_nodes()
|
||||
|
||||
self.assertNotEqual(master_nodes, [])
|
||||
self.assertNotEqual(replica_nodes, [])
|
||||
|
||||
# taint node with postgres=:NoExecute to force failover
|
||||
body = {
|
||||
"spec": {
|
||||
"taints": [
|
||||
{
|
||||
"effect": "NoExecute",
|
||||
"key": "postgres"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
k8s.api.core_v1.patch_node(master_nodes[0], body)
|
||||
self.eventuallyTrue(lambda: k8s.get_cluster_nodes()[0], replica_nodes)
|
||||
self.assertNotEqual(lambda: k8s.get_cluster_nodes()[0], master_nodes)
|
||||
|
||||
# add toleration to pods
|
||||
patch_toleration_config = {
|
||||
"data": {
|
||||
"toleration": "key:postgres,operator:Exists,effect:NoExecute"
|
||||
}
|
||||
}
|
||||
|
||||
k8s.update_config(patch_toleration_config, step="allow tainted nodes")
|
||||
|
||||
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2, "No 2 pods running")
|
||||
self.eventuallyEqual(lambda: len(k8s.get_patroni_running_members("acid-minimal-cluster-0")), 2, "Postgres status did not enter running")
|
||||
|
||||
# toggle pod anti affinity to move replica away from master node
|
||||
nm, new_replica_nodes = k8s.get_cluster_nodes()
|
||||
new_master_node = nm[0]
|
||||
self.assert_distributed_pods(new_master_node, new_replica_nodes, cluster_label)
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_zz_cluster_deletion(self):
|
||||
'''
|
||||
Test deletion with configured protection
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ metadata:
|
|||
# "delete-date": "2020-08-31" # can only be deleted on that day if "delete-date "key is configured
|
||||
# "delete-clustername": "acid-test-cluster" # can only be deleted when name matches if "delete-clustername" key is configured
|
||||
spec:
|
||||
dockerImage: registry.opensource.zalan.do/acid/spilo-13:2.0-p7
|
||||
dockerImage: registry.opensource.zalan.do/acid/spilo-13:2.1-p1
|
||||
teamId: "acid"
|
||||
numberOfInstances: 2
|
||||
users: # Application/Robot users
|
||||
|
|
@ -46,6 +46,12 @@ spec:
|
|||
# storageClass: my-sc
|
||||
# iops: 1000 # for EBS gp3
|
||||
# throughput: 250 # in MB/s for EBS gp3
|
||||
# selector:
|
||||
# matchExpressions:
|
||||
# - { key: flavour, operator: In, values: [ "banana", "chocolate" ] }
|
||||
# matchLabels:
|
||||
# environment: dev
|
||||
# service: postgres
|
||||
additionalVolumes:
|
||||
- name: empty
|
||||
mountPath: /opt/empty
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ data:
|
|||
# connection_pooler_default_cpu_request: "500m"
|
||||
# connection_pooler_default_memory_limit: 100Mi
|
||||
# connection_pooler_default_memory_request: 100Mi
|
||||
connection_pooler_image: "registry.opensource.zalan.do/acid/pgbouncer:master-16"
|
||||
connection_pooler_image: "registry.opensource.zalan.do/acid/pgbouncer:master-18"
|
||||
# connection_pooler_max_db_connections: 60
|
||||
# connection_pooler_mode: "transaction"
|
||||
# connection_pooler_number_of_instances: 2
|
||||
|
|
@ -32,10 +32,11 @@ data:
|
|||
# default_memory_request: 100Mi
|
||||
# delete_annotation_date_key: delete-date
|
||||
# delete_annotation_name_key: delete-clustername
|
||||
docker_image: registry.opensource.zalan.do/acid/spilo-13:2.0-p7
|
||||
docker_image: registry.opensource.zalan.do/acid/spilo-13:2.1-p1
|
||||
# downscaler_annotations: "deployment-time,downscaler/*"
|
||||
# enable_admin_role_for_users: "true"
|
||||
# enable_crd_validation: "true"
|
||||
# enable_cross_namespace_secret: "false"
|
||||
# enable_database_access: "true"
|
||||
enable_ebs_gp3_migration: "false"
|
||||
# enable_ebs_gp3_migration_max_size: "1000"
|
||||
|
|
@ -64,7 +65,7 @@ data:
|
|||
# inherited_labels: application,environment
|
||||
# kube_iam_role: ""
|
||||
# log_s3_bucket: ""
|
||||
logical_backup_docker_image: "registry.opensource.zalan.do/acid/logical-backup:v1.6.3"
|
||||
logical_backup_docker_image: "registry.opensource.zalan.do/acid/logical-backup:v1.7.0"
|
||||
# logical_backup_google_application_credentials: ""
|
||||
logical_backup_job_prefix: "logical-backup-"
|
||||
logical_backup_provider: "s3"
|
||||
|
|
@ -128,6 +129,7 @@ data:
|
|||
# team_api_role_configuration: "log_statement:all"
|
||||
# teams_api_url: http://fake-teams-api.default.svc.cluster.local
|
||||
# toleration: ""
|
||||
# wal_az_storage_account: ""
|
||||
# wal_gs_bucket: ""
|
||||
# wal_s3_bucket: ""
|
||||
watched_namespace: "*" # listen to all namespaces
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ spec:
|
|||
serviceAccountName: postgres-operator
|
||||
containers:
|
||||
- name: postgres-operator
|
||||
image: registry.opensource.zalan.do/acid/pgbouncer:master-16
|
||||
image: registry.opensource.zalan.do/acid/pgbouncer:master-18
|
||||
imagePullPolicy: IfNotPresent
|
||||
resources:
|
||||
requests:
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ spec:
|
|||
properties:
|
||||
docker_image:
|
||||
type: string
|
||||
default: "registry.opensource.zalan.do/acid/spilo-13:2.0-p7"
|
||||
default: "registry.opensource.zalan.do/acid/spilo-13:2.1-p1"
|
||||
enable_crd_validation:
|
||||
type: boolean
|
||||
default: true
|
||||
|
|
@ -384,6 +384,8 @@ spec:
|
|||
type: string
|
||||
log_s3_bucket:
|
||||
type: string
|
||||
wal_az_storage_account:
|
||||
type: string
|
||||
wal_gs_bucket:
|
||||
type: string
|
||||
wal_s3_bucket:
|
||||
|
|
@ -393,7 +395,7 @@ spec:
|
|||
properties:
|
||||
logical_backup_docker_image:
|
||||
type: string
|
||||
default: "registry.opensource.zalan.do/acid/logical-backup:v1.6.3"
|
||||
default: "registry.opensource.zalan.do/acid/logical-backup:v1.7.0"
|
||||
logical_backup_google_application_credentials:
|
||||
type: string
|
||||
logical_backup_job_prefix:
|
||||
|
|
@ -528,7 +530,7 @@ spec:
|
|||
default: "pooler"
|
||||
connection_pooler_image:
|
||||
type: string
|
||||
default: "registry.opensource.zalan.do/acid/pgbouncer:master-16"
|
||||
default: "registry.opensource.zalan.do/acid/pgbouncer:master-18"
|
||||
connection_pooler_max_db_connections:
|
||||
type: integer
|
||||
default: 60
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ spec:
|
|||
serviceAccountName: postgres-operator
|
||||
containers:
|
||||
- name: postgres-operator
|
||||
image: registry.opensource.zalan.do/acid/postgres-operator:v1.6.3
|
||||
image: registry.opensource.zalan.do/acid/postgres-operator:v1.7.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
resources:
|
||||
requests:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ kind: OperatorConfiguration
|
|||
metadata:
|
||||
name: postgresql-operator-default-configuration
|
||||
configuration:
|
||||
docker_image: registry.opensource.zalan.do/acid/spilo-13:2.0-p7
|
||||
docker_image: registry.opensource.zalan.do/acid/spilo-13:2.1-p1
|
||||
# enable_crd_validation: true
|
||||
# enable_lazy_spilo_upgrade: false
|
||||
enable_pgversion_env_var: true
|
||||
|
|
@ -45,6 +45,7 @@ configuration:
|
|||
# downscaler_annotations:
|
||||
# - deployment-time
|
||||
# - downscaler/*
|
||||
# enable_cross_namespace_secret: "false"
|
||||
enable_init_containers: true
|
||||
enable_pod_antiaffinity: false
|
||||
enable_pod_disruption_budget: true
|
||||
|
|
@ -120,10 +121,11 @@ configuration:
|
|||
# gcp_credentials: ""
|
||||
# kube_iam_role: ""
|
||||
# log_s3_bucket: ""
|
||||
# wal_az_storage_account: ""
|
||||
# wal_gs_bucket: ""
|
||||
# wal_s3_bucket: ""
|
||||
logical_backup:
|
||||
logical_backup_docker_image: "registry.opensource.zalan.do/acid/logical-backup:v1.6.3"
|
||||
logical_backup_docker_image: "registry.opensource.zalan.do/acid/logical-backup:v1.7.0"
|
||||
# logical_backup_google_application_credentials: ""
|
||||
logical_backup_job_prefix: "logical-backup-"
|
||||
logical_backup_provider: "s3"
|
||||
|
|
@ -164,7 +166,7 @@ configuration:
|
|||
connection_pooler_default_cpu_request: "500m"
|
||||
connection_pooler_default_memory_limit: 100Mi
|
||||
connection_pooler_default_memory_request: 100Mi
|
||||
connection_pooler_image: "registry.opensource.zalan.do/acid/pgbouncer:master-16"
|
||||
connection_pooler_image: "registry.opensource.zalan.do/acid/pgbouncer:master-18"
|
||||
# connection_pooler_max_db_connections: 60
|
||||
connection_pooler_mode: "transaction"
|
||||
connection_pooler_number_of_instances: 2
|
||||
|
|
|
|||
|
|
@ -219,6 +219,97 @@ spec:
|
|||
items:
|
||||
type: string
|
||||
pattern: '^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ *$'
|
||||
nodeAffinity:
|
||||
type: object
|
||||
properties:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- weight
|
||||
- preference
|
||||
properties:
|
||||
preference:
|
||||
type: object
|
||||
properties:
|
||||
matchExpressions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
matchFields:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
weight:
|
||||
format: int32
|
||||
type: integer
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
type: object
|
||||
required:
|
||||
- nodeSelectorTerms
|
||||
properties:
|
||||
nodeSelectorTerms:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
matchExpressions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
matchFields:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
numberOfInstances:
|
||||
type: integer
|
||||
minimum: 0
|
||||
|
|
@ -299,6 +390,8 @@ spec:
|
|||
type: boolean
|
||||
defaultRoles:
|
||||
type: boolean
|
||||
secretNamespace:
|
||||
type: string
|
||||
replicaLoadBalancer: # deprecated
|
||||
type: boolean
|
||||
resources:
|
||||
|
|
@ -392,97 +485,6 @@ spec:
|
|||
type: string
|
||||
caSecretName:
|
||||
type: string
|
||||
nodeAffinity:
|
||||
type: object
|
||||
properties:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- weight
|
||||
- preference
|
||||
properties:
|
||||
preference:
|
||||
type: object
|
||||
properties:
|
||||
matchExpressions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
matchFields:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
weight:
|
||||
format: int32
|
||||
type: integer
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
type: object
|
||||
required:
|
||||
- nodeSelectorTerms
|
||||
properties:
|
||||
nodeSelectorTerms:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
matchExpressions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
matchFields:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
tolerations:
|
||||
type: array
|
||||
items:
|
||||
|
|
@ -555,6 +557,24 @@ spec:
|
|||
properties:
|
||||
iops:
|
||||
type: integer
|
||||
selector:
|
||||
type: object
|
||||
properties:
|
||||
matchExpressions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
operator:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
matchLabels:
|
||||
type: object
|
||||
size:
|
||||
type: string
|
||||
pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$'
|
||||
|
|
|
|||
|
|
@ -341,6 +341,91 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
|
|||
},
|
||||
},
|
||||
},
|
||||
"nodeAffinity": {
|
||||
Type: "object",
|
||||
Properties: map[string]apiextv1.JSONSchemaProps{
|
||||
"preferredDuringSchedulingIgnoredDuringExecution": {
|
||||
Type: "array",
|
||||
Items: &apiextv1.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Required: []string{"preference", "weight"},
|
||||
Properties: map[string]apiextv1.JSONSchemaProps{
|
||||
"preference": {
|
||||
Type: "object",
|
||||
Properties: map[string]apiextv1.JSONSchemaProps{
|
||||
"matchExpressions": {
|
||||
Type: "array",
|
||||
Items: &apiextv1.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{
|
||||
Allows: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"matchFields": {
|
||||
Type: "array",
|
||||
Items: &apiextv1.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{
|
||||
Allows: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"weight": {
|
||||
Type: "integer",
|
||||
Format: "int32",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"requiredDuringSchedulingIgnoredDuringExecution": {
|
||||
Type: "object",
|
||||
Required: []string{"nodeSelectorTerms"},
|
||||
Properties: map[string]apiextv1.JSONSchemaProps{
|
||||
"nodeSelectorTerms": {
|
||||
Type: "array",
|
||||
Items: &apiextv1.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextv1.JSONSchemaProps{
|
||||
"matchExpressions": {
|
||||
Type: "array",
|
||||
Items: &apiextv1.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{
|
||||
Allows: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"matchFields": {
|
||||
Type: "array",
|
||||
Items: &apiextv1.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{
|
||||
Allows: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"numberOfInstances": {
|
||||
Type: "integer",
|
||||
Minimum: &min0,
|
||||
|
|
@ -488,6 +573,9 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
|
|||
},
|
||||
},
|
||||
},
|
||||
"secretNamespace": {
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -596,91 +684,6 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
|
|||
},
|
||||
},
|
||||
},
|
||||
"nodeAffinity": {
|
||||
Type: "object",
|
||||
Properties: map[string]apiextv1.JSONSchemaProps{
|
||||
"preferredDuringSchedulingIgnoredDuringExecution": {
|
||||
Type: "array",
|
||||
Items: &apiextv1.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Required: []string{"preference", "weight"},
|
||||
Properties: map[string]apiextv1.JSONSchemaProps{
|
||||
"preference": {
|
||||
Type: "object",
|
||||
Properties: map[string]apiextv1.JSONSchemaProps{
|
||||
"matchExpressions": {
|
||||
Type: "array",
|
||||
Items: &apiextv1.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{
|
||||
Allows: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"matchFields": {
|
||||
Type: "array",
|
||||
Items: &apiextv1.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{
|
||||
Allows: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"weight": {
|
||||
Type: "integer",
|
||||
Format: "int32",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"requiredDuringSchedulingIgnoredDuringExecution": {
|
||||
Type: "object",
|
||||
Required: []string{"nodeSelectorTerms"},
|
||||
Properties: map[string]apiextv1.JSONSchemaProps{
|
||||
"nodeSelectorTerms": {
|
||||
Type: "array",
|
||||
Items: &apiextv1.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextv1.JSONSchemaProps{
|
||||
"matchExpressions": {
|
||||
Type: "array",
|
||||
Items: &apiextv1.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{
|
||||
Allows: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"matchFields": {
|
||||
Type: "array",
|
||||
Items: &apiextv1.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{
|
||||
Allows: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"tolerations": {
|
||||
Type: "array",
|
||||
Items: &apiextv1.JSONSchemaPropsOrArray{
|
||||
|
|
@ -838,6 +841,54 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
|
|||
"iops": {
|
||||
Type: "integer",
|
||||
},
|
||||
"selector": {
|
||||
Type: "object",
|
||||
Properties: map[string]apiextv1.JSONSchemaProps{
|
||||
"matchExpressions": {
|
||||
Type: "array",
|
||||
Items: &apiextv1.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Required: []string{"key", "operator", "values"},
|
||||
Properties: map[string]apiextv1.JSONSchemaProps{
|
||||
"key": {
|
||||
Type: "string",
|
||||
},
|
||||
"operator": {
|
||||
Type: "string",
|
||||
Enum: []apiextv1.JSON{
|
||||
{
|
||||
Raw: []byte(`"In"`),
|
||||
},
|
||||
{
|
||||
Raw: []byte(`"NotIn"`),
|
||||
},
|
||||
{
|
||||
Raw: []byte(`"Exists"`),
|
||||
},
|
||||
{
|
||||
Raw: []byte(`"DoesNotExist"`),
|
||||
},
|
||||
},
|
||||
},
|
||||
"values": {
|
||||
Type: "array",
|
||||
Items: &apiextv1.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextv1.JSONSchemaProps{
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"matchLabels": {
|
||||
Type: "object",
|
||||
XPreserveUnknownFields: util.True(),
|
||||
},
|
||||
},
|
||||
},
|
||||
"size": {
|
||||
Type: "string",
|
||||
Description: "Value must not be zero",
|
||||
|
|
@ -1026,6 +1077,9 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{
|
|||
},
|
||||
},
|
||||
},
|
||||
"enable_cross_namespace_secret": {
|
||||
Type: "boolean",
|
||||
},
|
||||
"enable_init_containers": {
|
||||
Type: "boolean",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ func (ps *PostgresStatus) UnmarshalJSON(data []byte) error {
|
|||
if err != nil {
|
||||
metaErr := json.Unmarshal(data, &status)
|
||||
if metaErr != nil {
|
||||
return fmt.Errorf("Could not parse status: %v; err %v", string(data), metaErr)
|
||||
return fmt.Errorf("could not parse status: %v; err %v", string(data), metaErr)
|
||||
}
|
||||
tmp.PostgresClusterStatus = status
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ type KubernetesMetaConfiguration struct {
|
|||
EnablePodAntiAffinity bool `json:"enable_pod_antiaffinity,omitempty"`
|
||||
PodAntiAffinityTopologyKey string `json:"pod_antiaffinity_topology_key,omitempty"`
|
||||
PodManagementPolicy string `json:"pod_management_policy,omitempty"`
|
||||
EnableCrossNamespaceSecret bool `json:"enable_cross_namespace_secret,omitempty"`
|
||||
}
|
||||
|
||||
// PostgresPodResourcesDefaults defines the spec of default resources
|
||||
|
|
@ -131,6 +132,7 @@ type AWSGCPConfiguration struct {
|
|||
AWSRegion string `json:"aws_region,omitempty"`
|
||||
WALGSBucket string `json:"wal_gs_bucket,omitempty"`
|
||||
GCPCredentials string `json:"gcp_credentials,omitempty"`
|
||||
WALAZStorageAccount string `json:"wal_az_storage_account,omitempty"`
|
||||
LogS3Bucket string `json:"log_s3_bucket,omitempty"`
|
||||
KubeIAMRole string `json:"kube_iam_role,omitempty"`
|
||||
AdditionalSecretMount string `json:"additional_secret_mount,omitempty"`
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ type PreparedDatabase struct {
|
|||
PreparedSchemas map[string]PreparedSchema `json:"schemas,omitempty"`
|
||||
DefaultUsers bool `json:"defaultUsers,omitempty" defaults:"false"`
|
||||
Extensions map[string]string `json:"extensions,omitempty"`
|
||||
SecretNamespace string `json:"secretNamespace,omitempty"`
|
||||
}
|
||||
|
||||
// PreparedSchema describes elements to be bootstrapped per schema
|
||||
|
|
@ -113,12 +114,13 @@ type MaintenanceWindow struct {
|
|||
|
||||
// Volume describes a single volume in the manifest.
|
||||
type Volume struct {
|
||||
Size string `json:"size"`
|
||||
StorageClass string `json:"storageClass,omitempty"`
|
||||
SubPath string `json:"subPath,omitempty"`
|
||||
Iops *int64 `json:"iops,omitempty"`
|
||||
Throughput *int64 `json:"throughput,omitempty"`
|
||||
VolumeType string `json:"type,omitempty"`
|
||||
Selector *metav1.LabelSelector `json:"selector,omitempty"`
|
||||
Size string `json:"size"`
|
||||
StorageClass string `json:"storageClass,omitempty"`
|
||||
SubPath string `json:"subPath,omitempty"`
|
||||
Iops *int64 `json:"iops,omitempty"`
|
||||
Throughput *int64 `json:"throughput,omitempty"`
|
||||
VolumeType string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// AdditionalVolume specs additional optional volumes for statefulset
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ package v1
|
|||
import (
|
||||
config "github.com/zalando/postgres-operator/pkg/util/config"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
|
|
@ -314,22 +315,6 @@ func (in *MaintenanceWindow) DeepCopy() *MaintenanceWindow {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *MajorVersionUpgradeConfiguration) DeepCopyInto(out *MajorVersionUpgradeConfiguration) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MajorVersionUpgradeConfiguration.
|
||||
func (in *MajorVersionUpgradeConfiguration) DeepCopy() *MajorVersionUpgradeConfiguration {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(MajorVersionUpgradeConfiguration)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *OperatorConfiguration) DeepCopyInto(out *OperatorConfiguration) {
|
||||
*out = *in
|
||||
|
|
@ -385,7 +370,6 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData
|
|||
}
|
||||
}
|
||||
out.PostgresUsersConfiguration = in.PostgresUsersConfiguration
|
||||
out.MajorVersionUpgrade = in.MajorVersionUpgrade
|
||||
in.Kubernetes.DeepCopyInto(&out.Kubernetes)
|
||||
out.PostgresPodResources = in.PostgresPodResources
|
||||
out.Timeouts = in.Timeouts
|
||||
|
|
@ -1197,6 +1181,11 @@ func (in UserFlags) DeepCopy() UserFlags {
|
|||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Volume) DeepCopyInto(out *Volume) {
|
||||
*out = *in
|
||||
if in.Selector != nil {
|
||||
in, out := &in.Selector, &out.Selector
|
||||
*out = new(metav1.LabelSelector)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Iops != nil {
|
||||
in, out := &in.Iops, &out.Iops
|
||||
*out = new(int64)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package cluster
|
|||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
|
|
@ -519,7 +520,7 @@ func (c *Cluster) compareContainers(description string, setA, setB []v1.Containe
|
|||
newCheck("new statefulset %s's %s (index %d) resources do not match the current ones",
|
||||
func(a, b v1.Container) bool { return !compareResources(&a.Resources, &b.Resources) }),
|
||||
newCheck("new statefulset %s's %s (index %d) environment does not match the current one",
|
||||
func(a, b v1.Container) bool { return !reflect.DeepEqual(a.Env, b.Env) }),
|
||||
func(a, b v1.Container) bool { return !compareEnv(a.Env, b.Env) }),
|
||||
newCheck("new statefulset %s's %s (index %d) environment sources do not match the current one",
|
||||
func(a, b v1.Container) bool { return !reflect.DeepEqual(a.EnvFrom, b.EnvFrom) }),
|
||||
newCheck("new statefulset %s's %s (index %d) security context does not match the current one",
|
||||
|
|
@ -576,6 +577,56 @@ func compareResourcesAssumeFirstNotNil(a *v1.ResourceRequirements, b *v1.Resourc
|
|||
|
||||
}
|
||||
|
||||
func compareEnv(a, b []v1.EnvVar) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
equal := true
|
||||
for _, enva := range a {
|
||||
hasmatch := false
|
||||
for _, envb := range b {
|
||||
if enva.Name == envb.Name {
|
||||
hasmatch = true
|
||||
if enva.Name == "SPILO_CONFIGURATION" {
|
||||
equal = compareSpiloConfiguration(enva.Value, envb.Value)
|
||||
} else {
|
||||
if enva.Value == "" && envb.Value == "" {
|
||||
equal = reflect.DeepEqual(enva.ValueFrom, envb.ValueFrom)
|
||||
} else {
|
||||
equal = (enva.Value == envb.Value)
|
||||
}
|
||||
}
|
||||
if !equal {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasmatch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func compareSpiloConfiguration(configa, configb string) bool {
|
||||
var (
|
||||
oa, ob spiloConfiguration
|
||||
)
|
||||
|
||||
var err error
|
||||
err = json.Unmarshal([]byte(configa), &oa)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
oa.Bootstrap.DCS = patroniDCS{}
|
||||
err = json.Unmarshal([]byte(configb), &ob)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ob.Bootstrap.DCS = patroniDCS{}
|
||||
return reflect.DeepEqual(oa, ob)
|
||||
}
|
||||
|
||||
func (c *Cluster) enforceMinResourceLimits(spec *acidv1.PostgresSpec) error {
|
||||
|
||||
var (
|
||||
|
|
@ -940,22 +991,24 @@ func (c *Cluster) initSystemUsers() {
|
|||
// secrets, therefore, setting flags like SUPERUSER or REPLICATION
|
||||
// is not necessary here
|
||||
c.systemUsers[constants.SuperuserKeyName] = spec.PgUser{
|
||||
Origin: spec.RoleOriginSystem,
|
||||
Name: c.OpConfig.SuperUsername,
|
||||
Password: util.RandomPassword(constants.PasswordLength),
|
||||
Origin: spec.RoleOriginSystem,
|
||||
Name: c.OpConfig.SuperUsername,
|
||||
Namespace: c.Namespace,
|
||||
Password: util.RandomPassword(constants.PasswordLength),
|
||||
}
|
||||
c.systemUsers[constants.ReplicationUserKeyName] = spec.PgUser{
|
||||
Origin: spec.RoleOriginSystem,
|
||||
Name: c.OpConfig.ReplicationUsername,
|
||||
Password: util.RandomPassword(constants.PasswordLength),
|
||||
Origin: spec.RoleOriginSystem,
|
||||
Name: c.OpConfig.ReplicationUsername,
|
||||
Namespace: c.Namespace,
|
||||
Password: util.RandomPassword(constants.PasswordLength),
|
||||
}
|
||||
|
||||
// Connection pooler user is an exception, if requested it's going to be
|
||||
// created by operator as a normal pgUser
|
||||
if needConnectionPooler(&c.Spec) {
|
||||
// initialize empty connection pooler if not done yet
|
||||
if c.Spec.ConnectionPooler == nil {
|
||||
c.Spec.ConnectionPooler = &acidv1.ConnectionPooler{}
|
||||
connectionPoolerSpec := c.Spec.ConnectionPooler
|
||||
if connectionPoolerSpec == nil {
|
||||
connectionPoolerSpec = &acidv1.ConnectionPooler{}
|
||||
}
|
||||
|
||||
// Using superuser as pooler user is not a good idea. First of all it's
|
||||
|
|
@ -963,22 +1016,23 @@ func (c *Cluster) initSystemUsers() {
|
|||
// and second it's a bad practice.
|
||||
username := c.OpConfig.ConnectionPooler.User
|
||||
|
||||
isSuperUser := c.Spec.ConnectionPooler.User == c.OpConfig.SuperUsername
|
||||
isSuperUser := connectionPoolerSpec.User == c.OpConfig.SuperUsername
|
||||
isProtectedUser := c.shouldAvoidProtectedOrSystemRole(
|
||||
c.Spec.ConnectionPooler.User, "connection pool role")
|
||||
connectionPoolerSpec.User, "connection pool role")
|
||||
|
||||
if !isSuperUser && !isProtectedUser {
|
||||
username = util.Coalesce(
|
||||
c.Spec.ConnectionPooler.User,
|
||||
connectionPoolerSpec.User,
|
||||
c.OpConfig.ConnectionPooler.User)
|
||||
}
|
||||
|
||||
// connection pooler application should be able to login with this role
|
||||
connectionPoolerUser := spec.PgUser{
|
||||
Origin: spec.RoleConnectionPooler,
|
||||
Name: username,
|
||||
Flags: []string{constants.RoleFlagLogin},
|
||||
Password: util.RandomPassword(constants.PasswordLength),
|
||||
Origin: spec.RoleConnectionPooler,
|
||||
Name: username,
|
||||
Namespace: c.Namespace,
|
||||
Flags: []string{constants.RoleFlagLogin},
|
||||
Password: util.RandomPassword(constants.PasswordLength),
|
||||
}
|
||||
|
||||
if _, exists := c.pgUsers[username]; !exists {
|
||||
|
|
@ -1023,11 +1077,11 @@ func (c *Cluster) initPreparedDatabaseRoles() error {
|
|||
}
|
||||
|
||||
// default roles per database
|
||||
if err := c.initDefaultRoles(defaultRoles, "admin", preparedDbName, searchPath.String()); err != nil {
|
||||
if err := c.initDefaultRoles(defaultRoles, "admin", preparedDbName, searchPath.String(), preparedDB.SecretNamespace); err != nil {
|
||||
return fmt.Errorf("could not initialize default roles for database %s: %v", preparedDbName, err)
|
||||
}
|
||||
if preparedDB.DefaultUsers {
|
||||
if err := c.initDefaultRoles(defaultUsers, "admin", preparedDbName, searchPath.String()); err != nil {
|
||||
if err := c.initDefaultRoles(defaultUsers, "admin", preparedDbName, searchPath.String(), preparedDB.SecretNamespace); err != nil {
|
||||
return fmt.Errorf("could not initialize default roles for database %s: %v", preparedDbName, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1038,14 +1092,14 @@ func (c *Cluster) initPreparedDatabaseRoles() error {
|
|||
if err := c.initDefaultRoles(defaultRoles,
|
||||
preparedDbName+constants.OwnerRoleNameSuffix,
|
||||
preparedDbName+"_"+preparedSchemaName,
|
||||
constants.DefaultSearchPath+", "+preparedSchemaName); err != nil {
|
||||
constants.DefaultSearchPath+", "+preparedSchemaName, preparedDB.SecretNamespace); err != nil {
|
||||
return fmt.Errorf("could not initialize default roles for database schema %s: %v", preparedSchemaName, err)
|
||||
}
|
||||
if preparedSchema.DefaultUsers {
|
||||
if err := c.initDefaultRoles(defaultUsers,
|
||||
preparedDbName+constants.OwnerRoleNameSuffix,
|
||||
preparedDbName+"_"+preparedSchemaName,
|
||||
constants.DefaultSearchPath+", "+preparedSchemaName); err != nil {
|
||||
constants.DefaultSearchPath+", "+preparedSchemaName, preparedDB.SecretNamespace); err != nil {
|
||||
return fmt.Errorf("could not initialize default users for database schema %s: %v", preparedSchemaName, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1055,10 +1109,19 @@ func (c *Cluster) initPreparedDatabaseRoles() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Cluster) initDefaultRoles(defaultRoles map[string]string, admin, prefix string, searchPath string) error {
|
||||
func (c *Cluster) initDefaultRoles(defaultRoles map[string]string, admin, prefix, searchPath, secretNamespace string) error {
|
||||
|
||||
for defaultRole, inherits := range defaultRoles {
|
||||
|
||||
namespace := c.Namespace
|
||||
//if namespaced secrets are allowed
|
||||
if secretNamespace != "" {
|
||||
if c.Config.OpConfig.EnableCrossNamespaceSecret {
|
||||
namespace = secretNamespace
|
||||
} else {
|
||||
c.logger.Warn("secretNamespace ignored because enable_cross_namespace_secret set to false. Creating secrets in cluster namespace.")
|
||||
}
|
||||
}
|
||||
roleName := prefix + defaultRole
|
||||
|
||||
flags := []string{constants.RoleFlagNoLogin}
|
||||
|
|
@ -1081,6 +1144,7 @@ func (c *Cluster) initDefaultRoles(defaultRoles map[string]string, admin, prefix
|
|||
newRole := spec.PgUser{
|
||||
Origin: spec.RoleOriginBootstrap,
|
||||
Name: roleName,
|
||||
Namespace: namespace,
|
||||
Password: util.RandomPassword(constants.PasswordLength),
|
||||
Flags: flags,
|
||||
MemberOf: memberOf,
|
||||
|
|
@ -1105,6 +1169,16 @@ func (c *Cluster) initRobotUsers() error {
|
|||
if c.shouldAvoidProtectedOrSystemRole(username, "manifest robot role") {
|
||||
continue
|
||||
}
|
||||
namespace := c.Namespace
|
||||
|
||||
//if namespaced secrets are allowed
|
||||
if c.Config.OpConfig.EnableCrossNamespaceSecret {
|
||||
if strings.Contains(username, ".") {
|
||||
splits := strings.Split(username, ".")
|
||||
namespace = splits[0]
|
||||
}
|
||||
}
|
||||
|
||||
flags, err := normalizeUserFlags(userFlags)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid flags for user %q: %v", username, err)
|
||||
|
|
@ -1116,6 +1190,7 @@ func (c *Cluster) initRobotUsers() error {
|
|||
newRole := spec.PgUser{
|
||||
Origin: spec.RoleOriginManifest,
|
||||
Name: username,
|
||||
Namespace: namespace,
|
||||
Password: util.RandomPassword(constants.PasswordLength),
|
||||
Flags: flags,
|
||||
AdminRole: adminRole,
|
||||
|
|
@ -1233,6 +1308,7 @@ func (c *Cluster) initInfrastructureRoles() error {
|
|||
return fmt.Errorf("invalid flags for user '%v': %v", username, err)
|
||||
}
|
||||
newRole.Flags = flags
|
||||
newRole.Namespace = c.Namespace
|
||||
|
||||
if currentRole, present := c.pgUsers[username]; present {
|
||||
c.pgUsers[username] = c.resolveNameConflict(¤tRole, &newRole)
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ import (
|
|||
|
||||
"github.com/sirupsen/logrus"
|
||||
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
|
||||
fakeacidv1 "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/fake"
|
||||
"github.com/zalando/postgres-operator/pkg/spec"
|
||||
"github.com/zalando/postgres-operator/pkg/util/config"
|
||||
"github.com/zalando/postgres-operator/pkg/util/constants"
|
||||
"github.com/zalando/postgres-operator/pkg/util/k8sutil"
|
||||
"github.com/zalando/postgres-operator/pkg/util/teams"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/tools/record"
|
||||
)
|
||||
|
||||
|
|
@ -79,8 +82,8 @@ func TestInitRobotUsers(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
manifestUsers: map[string]acidv1.UserFlags{"foo": {"superuser", "createdb"}},
|
||||
infraRoles: map[string]spec.PgUser{"foo": {Origin: spec.RoleOriginInfrastructure, Name: "foo", Password: "bar"}},
|
||||
result: map[string]spec.PgUser{"foo": {Origin: spec.RoleOriginInfrastructure, Name: "foo", Password: "bar"}},
|
||||
infraRoles: map[string]spec.PgUser{"foo": {Origin: spec.RoleOriginInfrastructure, Name: "foo", Namespace: cl.Namespace, Password: "bar"}},
|
||||
result: map[string]spec.PgUser{"foo": {Origin: spec.RoleOriginInfrastructure, Name: "foo", Namespace: cl.Namespace, Password: "bar"}},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
|
|
@ -845,3 +848,243 @@ func TestPreparedDatabases(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareSpiloConfiguration(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Config string
|
||||
ExpectedResult bool
|
||||
}{
|
||||
{
|
||||
`{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"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":{"test":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"100","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`,
|
||||
true,
|
||||
},
|
||||
{
|
||||
`{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"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":{"test":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"200","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`,
|
||||
true,
|
||||
},
|
||||
{
|
||||
`{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"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":{"test":{"password":"","options":["CREATEDB"]}},"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"200","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`,
|
||||
false,
|
||||
},
|
||||
{
|
||||
`{}`,
|
||||
false,
|
||||
},
|
||||
{
|
||||
`invalidjson`,
|
||||
false,
|
||||
},
|
||||
}
|
||||
refCase := testCases[0]
|
||||
for _, testCase := range testCases {
|
||||
if result := compareSpiloConfiguration(refCase.Config, testCase.Config); result != testCase.ExpectedResult {
|
||||
t.Errorf("expected %v got %v", testCase.ExpectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareEnv(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Envs []v1.EnvVar
|
||||
ExpectedResult bool
|
||||
}{
|
||||
{
|
||||
Envs: []v1.EnvVar{
|
||||
{
|
||||
Name: "VARIABLE1",
|
||||
Value: "value1",
|
||||
},
|
||||
{
|
||||
Name: "VARIABLE2",
|
||||
Value: "value2",
|
||||
},
|
||||
{
|
||||
Name: "VARIABLE3",
|
||||
Value: "value3",
|
||||
},
|
||||
{
|
||||
Name: "SPILO_CONFIGURATION",
|
||||
Value: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"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":{"test":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"100","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`,
|
||||
},
|
||||
},
|
||||
ExpectedResult: true,
|
||||
},
|
||||
{
|
||||
Envs: []v1.EnvVar{
|
||||
{
|
||||
Name: "VARIABLE1",
|
||||
Value: "value1",
|
||||
},
|
||||
{
|
||||
Name: "VARIABLE2",
|
||||
Value: "value2",
|
||||
},
|
||||
{
|
||||
Name: "VARIABLE3",
|
||||
Value: "value3",
|
||||
},
|
||||
{
|
||||
Name: "SPILO_CONFIGURATION",
|
||||
Value: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"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":{"test":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`,
|
||||
},
|
||||
},
|
||||
ExpectedResult: true,
|
||||
},
|
||||
{
|
||||
Envs: []v1.EnvVar{
|
||||
{
|
||||
Name: "VARIABLE4",
|
||||
Value: "value4",
|
||||
},
|
||||
{
|
||||
Name: "VARIABLE2",
|
||||
Value: "value2",
|
||||
},
|
||||
{
|
||||
Name: "VARIABLE3",
|
||||
Value: "value3",
|
||||
},
|
||||
{
|
||||
Name: "SPILO_CONFIGURATION",
|
||||
Value: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"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":{"test":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`,
|
||||
},
|
||||
},
|
||||
ExpectedResult: false,
|
||||
},
|
||||
{
|
||||
Envs: []v1.EnvVar{
|
||||
{
|
||||
Name: "VARIABLE1",
|
||||
Value: "value1",
|
||||
},
|
||||
{
|
||||
Name: "VARIABLE2",
|
||||
Value: "value2",
|
||||
},
|
||||
{
|
||||
Name: "VARIABLE3",
|
||||
Value: "value3",
|
||||
},
|
||||
{
|
||||
Name: "VARIABLE4",
|
||||
Value: "value4",
|
||||
},
|
||||
{
|
||||
Name: "SPILO_CONFIGURATION",
|
||||
Value: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"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":{"test":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"100","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`,
|
||||
},
|
||||
},
|
||||
ExpectedResult: false,
|
||||
},
|
||||
{
|
||||
Envs: []v1.EnvVar{
|
||||
{
|
||||
Name: "VARIABLE1",
|
||||
Value: "value1",
|
||||
},
|
||||
{
|
||||
Name: "VARIABLE2",
|
||||
Value: "value2",
|
||||
},
|
||||
{
|
||||
Name: "SPILO_CONFIGURATION",
|
||||
Value: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/12/bin","parameters":{"autovacuum_analyze_scale_factor":"0.1"},"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":{"test":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"postgresql":{"parameters":{"max_connections":"100","max_locks_per_transaction":"64","max_worker_processes":"4"}}}}}`,
|
||||
},
|
||||
},
|
||||
ExpectedResult: false,
|
||||
},
|
||||
}
|
||||
refCase := testCases[0]
|
||||
for _, testCase := range testCases {
|
||||
if result := compareEnv(refCase.Envs, testCase.Envs); result != testCase.ExpectedResult {
|
||||
t.Errorf("expected %v got %v", testCase.ExpectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossNamespacedSecrets(t *testing.T) {
|
||||
testName := "test secrets in different namespace"
|
||||
clientSet := fake.NewSimpleClientset()
|
||||
acidClientSet := fakeacidv1.NewSimpleClientset()
|
||||
namespace := "default"
|
||||
|
||||
client := k8sutil.KubernetesClient{
|
||||
StatefulSetsGetter: clientSet.AppsV1(),
|
||||
ServicesGetter: clientSet.CoreV1(),
|
||||
DeploymentsGetter: clientSet.AppsV1(),
|
||||
PostgresqlsGetter: acidClientSet.AcidV1(),
|
||||
SecretsGetter: clientSet.CoreV1(),
|
||||
}
|
||||
pg := acidv1.Postgresql{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "acid-fake-cluster",
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: acidv1.PostgresSpec{
|
||||
Volume: acidv1.Volume{
|
||||
Size: "1Gi",
|
||||
},
|
||||
Users: map[string]acidv1.UserFlags{
|
||||
"appspace.db_user": {},
|
||||
"db_user": {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var cluster = New(
|
||||
Config{
|
||||
OpConfig: config.Config{
|
||||
ConnectionPooler: config.ConnectionPooler{
|
||||
ConnectionPoolerDefaultCPURequest: "100m",
|
||||
ConnectionPoolerDefaultCPULimit: "100m",
|
||||
ConnectionPoolerDefaultMemoryRequest: "100Mi",
|
||||
ConnectionPoolerDefaultMemoryLimit: "100Mi",
|
||||
NumberOfInstances: int32ToPointer(1),
|
||||
},
|
||||
PodManagementPolicy: "ordered_ready",
|
||||
Resources: config.Resources{
|
||||
ClusterLabels: map[string]string{"application": "spilo"},
|
||||
ClusterNameLabel: "cluster-name",
|
||||
DefaultCPURequest: "300m",
|
||||
DefaultCPULimit: "300m",
|
||||
DefaultMemoryRequest: "300Mi",
|
||||
DefaultMemoryLimit: "300Mi",
|
||||
PodRoleLabel: "spilo-role",
|
||||
},
|
||||
EnableCrossNamespaceSecret: true,
|
||||
},
|
||||
}, client, pg, logger, eventRecorder)
|
||||
|
||||
userNamespaceMap := map[string]string{
|
||||
cluster.Namespace: "db_user",
|
||||
"appspace": "appspace.db_user",
|
||||
}
|
||||
|
||||
err := cluster.initRobotUsers()
|
||||
if err != nil {
|
||||
t.Errorf("Could not create secret for namespaced users with error: %s", err)
|
||||
}
|
||||
|
||||
for _, u := range cluster.pgUsers {
|
||||
if u.Name != userNamespaceMap[u.Namespace] {
|
||||
t.Errorf("%s: Could not create namespaced user in its correct namespaces for user %s in namespace %s", testName, u.Name, u.Namespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidUsernames(t *testing.T) {
|
||||
testName := "test username validity"
|
||||
|
||||
invalidUsernames := []string{"_", ".", ".user", "appspace.", "user_", "_user", "-user", "user-", ",", "-", ",user", "user,", "namespace,user"}
|
||||
validUsernames := []string{"user", "appspace.user", "appspace.dot.user", "user_name", "app_space.user_name"}
|
||||
for _, username := range invalidUsernames {
|
||||
if isValidUsername(username) {
|
||||
t.Errorf("%s Invalid username is allowed: %s", testName, username)
|
||||
}
|
||||
}
|
||||
for _, username := range validUsernames {
|
||||
if !isValidUsername(username) {
|
||||
t.Errorf("%s Valid username is not allowed: %s", testName, username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package cluster
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/r3labs/diff"
|
||||
|
|
@ -60,7 +61,7 @@ func needMasterConnectionPooler(spec *acidv1.PostgresSpec) bool {
|
|||
}
|
||||
|
||||
func needMasterConnectionPoolerWorker(spec *acidv1.PostgresSpec) bool {
|
||||
return (nil != spec.EnableConnectionPooler && *spec.EnableConnectionPooler) ||
|
||||
return (spec.EnableConnectionPooler != nil && *spec.EnableConnectionPooler) ||
|
||||
(spec.ConnectionPooler != nil && spec.EnableConnectionPooler == nil)
|
||||
}
|
||||
|
||||
|
|
@ -114,7 +115,7 @@ func (c *Cluster) createConnectionPooler(LookupFunction InstallFunction) (SyncRe
|
|||
c.setProcessName("creating connection pooler")
|
||||
|
||||
//this is essentially sync with nil as oldSpec
|
||||
if reason, err := c.syncConnectionPooler(nil, &c.Postgresql, LookupFunction); err != nil {
|
||||
if reason, err := c.syncConnectionPooler(&acidv1.Postgresql{}, &c.Postgresql, LookupFunction); err != nil {
|
||||
return reason, err
|
||||
}
|
||||
return reason, nil
|
||||
|
|
@ -140,11 +141,15 @@ func (c *Cluster) createConnectionPooler(LookupFunction InstallFunction) (SyncRe
|
|||
// RESERVE_SIZE is how many additional connections to allow for a pooler.
|
||||
func (c *Cluster) getConnectionPoolerEnvVars() []v1.EnvVar {
|
||||
spec := &c.Spec
|
||||
connectionPoolerSpec := spec.ConnectionPooler
|
||||
if connectionPoolerSpec == nil {
|
||||
connectionPoolerSpec = &acidv1.ConnectionPooler{}
|
||||
}
|
||||
effectiveMode := util.Coalesce(
|
||||
spec.ConnectionPooler.Mode,
|
||||
connectionPoolerSpec.Mode,
|
||||
c.OpConfig.ConnectionPooler.Mode)
|
||||
|
||||
numberOfInstances := spec.ConnectionPooler.NumberOfInstances
|
||||
numberOfInstances := connectionPoolerSpec.NumberOfInstances
|
||||
if numberOfInstances == nil {
|
||||
numberOfInstances = util.CoalesceInt32(
|
||||
c.OpConfig.ConnectionPooler.NumberOfInstances,
|
||||
|
|
@ -152,7 +157,7 @@ func (c *Cluster) getConnectionPoolerEnvVars() []v1.EnvVar {
|
|||
}
|
||||
|
||||
effectiveMaxDBConn := util.CoalesceInt32(
|
||||
spec.ConnectionPooler.MaxDBConnections,
|
||||
connectionPoolerSpec.MaxDBConnections,
|
||||
c.OpConfig.ConnectionPooler.MaxDBConnections)
|
||||
|
||||
if effectiveMaxDBConn == nil {
|
||||
|
|
@ -201,17 +206,21 @@ func (c *Cluster) getConnectionPoolerEnvVars() []v1.EnvVar {
|
|||
func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) (
|
||||
*v1.PodTemplateSpec, error) {
|
||||
spec := &c.Spec
|
||||
connectionPoolerSpec := spec.ConnectionPooler
|
||||
if connectionPoolerSpec == nil {
|
||||
connectionPoolerSpec = &acidv1.ConnectionPooler{}
|
||||
}
|
||||
gracePeriod := int64(c.OpConfig.PodTerminateGracePeriod.Seconds())
|
||||
resources, err := generateResourceRequirements(
|
||||
spec.ConnectionPooler.Resources,
|
||||
connectionPoolerSpec.Resources,
|
||||
makeDefaultConnectionPoolerResources(&c.OpConfig))
|
||||
|
||||
effectiveDockerImage := util.Coalesce(
|
||||
spec.ConnectionPooler.DockerImage,
|
||||
connectionPoolerSpec.DockerImage,
|
||||
c.OpConfig.ConnectionPooler.Image)
|
||||
|
||||
effectiveSchema := util.Coalesce(
|
||||
spec.ConnectionPooler.Schema,
|
||||
connectionPoolerSpec.Schema,
|
||||
c.OpConfig.ConnectionPooler.Schema)
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -220,7 +229,7 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) (
|
|||
|
||||
secretSelector := func(key string) *v1.SecretKeySelector {
|
||||
effectiveUser := util.Coalesce(
|
||||
spec.ConnectionPooler.User,
|
||||
connectionPoolerSpec.User,
|
||||
c.OpConfig.ConnectionPooler.User)
|
||||
|
||||
return &v1.SecretKeySelector{
|
||||
|
|
@ -285,6 +294,8 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) (
|
|||
},
|
||||
}
|
||||
|
||||
tolerationsSpec := tolerations(&spec.Tolerations, c.OpConfig.PodToleration)
|
||||
|
||||
podTemplate := &v1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: c.connectionPoolerLabels(role, true).MatchLabels,
|
||||
|
|
@ -294,12 +305,18 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) (
|
|||
Spec: v1.PodSpec{
|
||||
TerminationGracePeriodSeconds: &gracePeriod,
|
||||
Containers: []v1.Container{poolerContainer},
|
||||
// TODO: add tolerations to scheduler pooler on the same node
|
||||
// as database
|
||||
//Tolerations: *tolerationsSpec,
|
||||
Tolerations: tolerationsSpec,
|
||||
},
|
||||
}
|
||||
|
||||
nodeAffinity := nodeAffinity(c.OpConfig.NodeReadinessLabel, spec.NodeAffinity)
|
||||
if c.OpConfig.EnablePodAntiAffinity {
|
||||
labelsSet := labels.Set(c.connectionPoolerLabels(role, false).MatchLabels)
|
||||
podTemplate.Spec.Affinity = generatePodAffinity(labelsSet, c.OpConfig.PodAntiAffinityTopologyKey, nodeAffinity)
|
||||
} else if nodeAffinity != nil {
|
||||
podTemplate.Spec.Affinity = nodeAffinity
|
||||
}
|
||||
|
||||
return podTemplate, nil
|
||||
}
|
||||
|
||||
|
|
@ -313,12 +330,13 @@ func (c *Cluster) generateConnectionPoolerDeployment(connectionPooler *Connectio
|
|||
// default values, initialize it to an empty structure. It could be done
|
||||
// anywhere, but here is the earliest common entry point between sync and
|
||||
// create code, so init here.
|
||||
if spec.ConnectionPooler == nil {
|
||||
spec.ConnectionPooler = &acidv1.ConnectionPooler{}
|
||||
connectionPoolerSpec := spec.ConnectionPooler
|
||||
if connectionPoolerSpec == nil {
|
||||
connectionPoolerSpec = &acidv1.ConnectionPooler{}
|
||||
}
|
||||
podTemplate, err := c.generateConnectionPoolerPodTemplate(connectionPooler.Role)
|
||||
|
||||
numberOfInstances := spec.ConnectionPooler.NumberOfInstances
|
||||
numberOfInstances := connectionPoolerSpec.NumberOfInstances
|
||||
if numberOfInstances == nil {
|
||||
numberOfInstances = util.CoalesceInt32(
|
||||
c.OpConfig.ConnectionPooler.NumberOfInstances,
|
||||
|
|
@ -363,16 +381,6 @@ func (c *Cluster) generateConnectionPoolerDeployment(connectionPooler *Connectio
|
|||
func (c *Cluster) generateConnectionPoolerService(connectionPooler *ConnectionPoolerObjects) *v1.Service {
|
||||
|
||||
spec := &c.Spec
|
||||
// there are two ways to enable connection pooler, either to specify a
|
||||
// connectionPooler section or enableConnectionPooler. In the second case
|
||||
// spec.connectionPooler will be nil, so to make it easier to calculate
|
||||
// default values, initialize it to an empty structure. It could be done
|
||||
// anywhere, but here is the earliest common entry point between sync and
|
||||
// create code, so init here.
|
||||
if spec.ConnectionPooler == nil {
|
||||
spec.ConnectionPooler = &acidv1.ConnectionPooler{}
|
||||
}
|
||||
|
||||
serviceSpec := v1.ServiceSpec{
|
||||
Ports: []v1.ServicePort{
|
||||
{
|
||||
|
|
@ -660,12 +668,14 @@ func makeDefaultConnectionPoolerResources(config *config.Config) acidv1.Resource
|
|||
|
||||
func logPoolerEssentials(log *logrus.Entry, oldSpec, newSpec *acidv1.Postgresql) {
|
||||
var v []string
|
||||
|
||||
var input []*bool
|
||||
|
||||
newMasterConnectionPoolerEnabled := needMasterConnectionPoolerWorker(&newSpec.Spec)
|
||||
if oldSpec == nil {
|
||||
input = []*bool{nil, nil, newSpec.Spec.EnableConnectionPooler, newSpec.Spec.EnableReplicaConnectionPooler}
|
||||
input = []*bool{nil, nil, &newMasterConnectionPoolerEnabled, newSpec.Spec.EnableReplicaConnectionPooler}
|
||||
} else {
|
||||
input = []*bool{oldSpec.Spec.EnableConnectionPooler, oldSpec.Spec.EnableReplicaConnectionPooler, newSpec.Spec.EnableConnectionPooler, newSpec.Spec.EnableReplicaConnectionPooler}
|
||||
oldMasterConnectionPoolerEnabled := needMasterConnectionPoolerWorker(&oldSpec.Spec)
|
||||
input = []*bool{&oldMasterConnectionPoolerEnabled, oldSpec.Spec.EnableReplicaConnectionPooler, &newMasterConnectionPoolerEnabled, newSpec.Spec.EnableReplicaConnectionPooler}
|
||||
}
|
||||
|
||||
for _, b := range input {
|
||||
|
|
@ -676,25 +686,16 @@ func logPoolerEssentials(log *logrus.Entry, oldSpec, newSpec *acidv1.Postgresql)
|
|||
}
|
||||
}
|
||||
|
||||
log.Debugf("syncing connection pooler from (%v, %v) to (%v, %v)", v[0], v[1], v[2], v[3])
|
||||
log.Debugf("syncing connection pooler (master, replica) from (%v, %v) to (%v, %v)", v[0], v[1], v[2], v[3])
|
||||
}
|
||||
|
||||
func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, LookupFunction InstallFunction) (SyncReason, error) {
|
||||
|
||||
var reason SyncReason
|
||||
var err error
|
||||
var newNeedConnectionPooler, oldNeedConnectionPooler bool
|
||||
oldNeedConnectionPooler = false
|
||||
var connectionPoolerNeeded bool
|
||||
|
||||
if oldSpec == nil {
|
||||
oldSpec = &acidv1.Postgresql{
|
||||
Spec: acidv1.PostgresSpec{
|
||||
ConnectionPooler: &acidv1.ConnectionPooler{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
needSync, _ := needSyncConnectionPoolerSpecs(oldSpec.Spec.ConnectionPooler, newSpec.Spec.ConnectionPooler, c.logger)
|
||||
needSync := !reflect.DeepEqual(oldSpec.Spec.ConnectionPooler, newSpec.Spec.ConnectionPooler)
|
||||
masterChanges, err := diff.Diff(oldSpec.Spec.EnableConnectionPooler, newSpec.Spec.EnableConnectionPooler)
|
||||
if err != nil {
|
||||
c.logger.Error("Error in getting diff of master connection pooler changes")
|
||||
|
|
@ -704,15 +705,14 @@ func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, Look
|
|||
c.logger.Error("Error in getting diff of replica connection pooler changes")
|
||||
}
|
||||
|
||||
// skip pooler sync only
|
||||
// 1. if there is no diff in spec, AND
|
||||
// 2. if connection pooler is already there and is also required as per newSpec
|
||||
//
|
||||
// Handling the case when connectionPooler is not there but it is required
|
||||
// skip pooler sync when theres no diff or it's deactivated
|
||||
// but, handling the case when connectionPooler is not there but it is required
|
||||
// as per spec, hence do not skip syncing in that case, even though there
|
||||
// is no diff in specs
|
||||
if (!needSync && len(masterChanges) <= 0 && len(replicaChanges) <= 0) &&
|
||||
(c.ConnectionPooler != nil && (needConnectionPooler(&newSpec.Spec))) {
|
||||
((!needConnectionPooler(&newSpec.Spec) && (c.ConnectionPooler == nil || !needConnectionPooler(&oldSpec.Spec))) ||
|
||||
(c.ConnectionPooler != nil && needConnectionPooler(&newSpec.Spec) &&
|
||||
(c.ConnectionPooler[Master].LookupFunction || c.ConnectionPooler[Replica].LookupFunction))) {
|
||||
c.logger.Debugln("syncing pooler is not required")
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -723,15 +723,9 @@ func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, Look
|
|||
for _, role := range [2]PostgresRole{Master, Replica} {
|
||||
|
||||
if role == Master {
|
||||
newNeedConnectionPooler = needMasterConnectionPoolerWorker(&newSpec.Spec)
|
||||
if oldSpec != nil {
|
||||
oldNeedConnectionPooler = needMasterConnectionPoolerWorker(&oldSpec.Spec)
|
||||
}
|
||||
connectionPoolerNeeded = needMasterConnectionPoolerWorker(&newSpec.Spec)
|
||||
} else {
|
||||
newNeedConnectionPooler = needReplicaConnectionPoolerWorker(&newSpec.Spec)
|
||||
if oldSpec != nil {
|
||||
oldNeedConnectionPooler = needReplicaConnectionPoolerWorker(&oldSpec.Spec)
|
||||
}
|
||||
connectionPoolerNeeded = needReplicaConnectionPoolerWorker(&newSpec.Spec)
|
||||
}
|
||||
|
||||
// if the call is via createConnectionPooler, then it is required to initialize
|
||||
|
|
@ -751,24 +745,22 @@ func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, Look
|
|||
}
|
||||
}
|
||||
|
||||
if newNeedConnectionPooler {
|
||||
if connectionPoolerNeeded {
|
||||
// Try to sync in any case. If we didn't needed connection pooler before,
|
||||
// it means we want to create it. If it was already present, still sync
|
||||
// since it could happen that there is no difference in specs, and all
|
||||
// the resources are remembered, but the deployment was manually deleted
|
||||
// in between
|
||||
|
||||
// in this case also do not forget to install lookup function as for
|
||||
// creating cluster
|
||||
if !oldNeedConnectionPooler || !c.ConnectionPooler[role].LookupFunction {
|
||||
newConnectionPooler := newSpec.Spec.ConnectionPooler
|
||||
|
||||
// in this case also do not forget to install lookup function
|
||||
if !c.ConnectionPooler[role].LookupFunction {
|
||||
connectionPooler := c.Spec.ConnectionPooler
|
||||
specSchema := ""
|
||||
specUser := ""
|
||||
|
||||
if newConnectionPooler != nil {
|
||||
specSchema = newConnectionPooler.Schema
|
||||
specUser = newConnectionPooler.User
|
||||
if connectionPooler != nil {
|
||||
specSchema = connectionPooler.Schema
|
||||
specUser = connectionPooler.User
|
||||
}
|
||||
|
||||
schema := util.Coalesce(
|
||||
|
|
@ -779,9 +771,10 @@ func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, Look
|
|||
specUser,
|
||||
c.OpConfig.ConnectionPooler.User)
|
||||
|
||||
if err = LookupFunction(schema, user, role); err != nil {
|
||||
if err = LookupFunction(schema, user); err != nil {
|
||||
return NoSync, err
|
||||
}
|
||||
c.ConnectionPooler[role].LookupFunction = true
|
||||
}
|
||||
|
||||
if reason, err = c.syncConnectionPoolerWorker(oldSpec, newSpec, role); err != nil {
|
||||
|
|
@ -800,8 +793,8 @@ func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, Look
|
|||
}
|
||||
}
|
||||
}
|
||||
if !needMasterConnectionPoolerWorker(&newSpec.Spec) &&
|
||||
!needReplicaConnectionPoolerWorker(&newSpec.Spec) {
|
||||
if (needMasterConnectionPoolerWorker(&oldSpec.Spec) || needReplicaConnectionPoolerWorker(&oldSpec.Spec)) &&
|
||||
!needMasterConnectionPoolerWorker(&newSpec.Spec) && !needReplicaConnectionPoolerWorker(&newSpec.Spec) {
|
||||
if err = c.deleteConnectionPoolerSecret(); err != nil {
|
||||
c.logger.Warningf("could not remove connection pooler secret: %v", err)
|
||||
}
|
||||
|
|
@ -866,8 +859,6 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql
|
|||
newConnectionPooler = &acidv1.ConnectionPooler{}
|
||||
}
|
||||
|
||||
c.logger.Infof("old: %+v, new %+v", oldConnectionPooler, newConnectionPooler)
|
||||
|
||||
var specSync bool
|
||||
var specReason []string
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import (
|
|||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func mockInstallLookupFunction(schema string, user string, role PostgresRole) error {
|
||||
func mockInstallLookupFunction(schema string, user string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -351,10 +351,30 @@ func (c *Cluster) execCreateDatabaseSchema(databaseName, schemaName, dbOwner, sc
|
|||
}
|
||||
|
||||
// set default privileges for schema
|
||||
// the schemaOwner defines them for global database roles
|
||||
c.execAlterSchemaDefaultPrivileges(schemaName, schemaOwner, databaseName)
|
||||
|
||||
// if schemaOwner and dbOwner differ we know that <databaseName>_<schemaName> default roles were created
|
||||
if schemaOwner != dbOwner {
|
||||
c.execAlterSchemaDefaultPrivileges(schemaName, dbOwner, databaseName+"_"+schemaName)
|
||||
c.execAlterSchemaDefaultPrivileges(schemaName, schemaOwner, databaseName+"_"+schemaName)
|
||||
defaultUsers := c.Spec.PreparedDatabases[databaseName].PreparedSchemas[schemaName].DefaultUsers
|
||||
|
||||
// define schema privileges of <databaseName>_<schemaName>_owner_user for global roles, too
|
||||
if defaultUsers {
|
||||
c.execAlterSchemaDefaultPrivileges(schemaName, schemaOwner+constants.UserRoleNameSuffix, databaseName)
|
||||
}
|
||||
|
||||
// collect all possible owner roles and define default schema privileges
|
||||
// for <databaseName>_<schemaName>_reader/writer roles
|
||||
owners := c.getOwnerRoles(databaseName, c.Spec.PreparedDatabases[databaseName].DefaultUsers)
|
||||
owners = append(owners, c.getOwnerRoles(databaseName+"_"+schemaName, defaultUsers)...)
|
||||
for _, owner := range owners {
|
||||
c.execAlterSchemaDefaultPrivileges(schemaName, owner, databaseName+"_"+schemaName)
|
||||
}
|
||||
} else {
|
||||
// define schema privileges of <databaseName>_owner_user for global roles, too
|
||||
if c.Spec.PreparedDatabases[databaseName].DefaultUsers {
|
||||
c.execAlterSchemaDefaultPrivileges(schemaName, schemaOwner+constants.UserRoleNameSuffix, databaseName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -418,6 +438,15 @@ func makeUserFlags(rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin
|
|||
return result
|
||||
}
|
||||
|
||||
func (c *Cluster) getOwnerRoles(dbObjPath string, withUser bool) (owners []string) {
|
||||
owners = append(owners, dbObjPath+constants.OwnerRoleNameSuffix)
|
||||
if withUser {
|
||||
owners = append(owners, dbObjPath+constants.OwnerRoleNameSuffix+constants.UserRoleNameSuffix)
|
||||
}
|
||||
|
||||
return owners
|
||||
}
|
||||
|
||||
// getExtension returns the list of current database extensions
|
||||
// The caller is responsible for opening and closing the database connection
|
||||
func (c *Cluster) getExtensions() (dbExtensions map[string]string, err error) {
|
||||
|
|
@ -479,7 +508,7 @@ func (c *Cluster) execCreateOrAlterExtension(extName, schemaName, statement, doi
|
|||
|
||||
// Creates a connection pool credentials lookup function in every database to
|
||||
// perform remote authentication.
|
||||
func (c *Cluster) installLookupFunction(poolerSchema, poolerUser string, role PostgresRole) error {
|
||||
func (c *Cluster) installLookupFunction(poolerSchema, poolerUser string) error {
|
||||
var stmtBytes bytes.Buffer
|
||||
|
||||
c.logger.Info("Installing lookup function")
|
||||
|
|
@ -575,8 +604,8 @@ func (c *Cluster) installLookupFunction(poolerSchema, poolerUser string, role Po
|
|||
c.logger.Infof("pooler lookup function installed into %s", dbname)
|
||||
}
|
||||
|
||||
if len(failedDatabases) == 0 {
|
||||
c.ConnectionPooler[role].LookupFunction = true
|
||||
if len(failedDatabases) > 0 {
|
||||
return fmt.Errorf("could not install pooler lookup function in every specified databases")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -412,13 +412,33 @@ func tolerations(tolerationsSpec *[]v1.Toleration, podToleration map[string]stri
|
|||
// 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 {
|
||||
return param == "max_connections" ||
|
||||
param == "max_locks_per_transaction" ||
|
||||
param == "max_worker_processes" ||
|
||||
param == "max_prepared_transactions" ||
|
||||
param == "wal_level" ||
|
||||
param == "wal_log_hints" ||
|
||||
param == "track_commit_timestamp"
|
||||
params := map[string]bool{
|
||||
"archive_command": false,
|
||||
"shared_buffers": false,
|
||||
"logging_collector": false,
|
||||
"log_destination": false,
|
||||
"log_directory": false,
|
||||
"log_filename": false,
|
||||
"log_file_mode": false,
|
||||
"log_rotation_age": false,
|
||||
"log_truncate_on_rotation": false,
|
||||
"ssl": false,
|
||||
"ssl_ca_file": false,
|
||||
"ssl_crl_file": false,
|
||||
"ssl_cert_file": false,
|
||||
"ssl_key_file": false,
|
||||
"shared_preload_libraries": false,
|
||||
"bg_mon.listen_address": false,
|
||||
"bg_mon.history_buckets": false,
|
||||
"pg_stat_statements.track_utility": false,
|
||||
"extwlist.extensions": false,
|
||||
"extwlist.custom_path": false,
|
||||
}
|
||||
result, ok := params[param]
|
||||
if !ok {
|
||||
result = true
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func generateVolumeMounts(volume acidv1.Volume) []v1.VolumeMount {
|
||||
|
|
@ -778,6 +798,12 @@ func (c *Cluster) generateSpiloPodEnvVars(uid types.UID, spiloConfiguration stri
|
|||
envVars = append(envVars, v1.EnvVar{Name: "WAL_BUCKET_SCOPE_PREFIX", Value: ""})
|
||||
}
|
||||
|
||||
if c.OpConfig.WALAZStorageAccount != "" {
|
||||
envVars = append(envVars, v1.EnvVar{Name: "AZURE_STORAGE_ACCOUNT", Value: c.OpConfig.WALAZStorageAccount})
|
||||
envVars = append(envVars, v1.EnvVar{Name: "WAL_BUCKET_SCOPE_SUFFIX", Value: getBucketScopeSuffix(string(uid))})
|
||||
envVars = append(envVars, v1.EnvVar{Name: "WAL_BUCKET_SCOPE_PREFIX", Value: ""})
|
||||
}
|
||||
|
||||
if c.OpConfig.GCPCredentials != "" {
|
||||
envVars = append(envVars, v1.EnvVar{Name: "GOOGLE_APPLICATION_CREDENTIALS", Value: c.OpConfig.GCPCredentials})
|
||||
}
|
||||
|
|
@ -1150,9 +1176,6 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
|
|||
}
|
||||
|
||||
// generate the spilo container
|
||||
c.logger.Debugf("Generating Spilo container, environment variables")
|
||||
c.logger.Debugf("%v", spiloEnvVars)
|
||||
|
||||
spiloContainer := generateContainer(constants.PostgresContainerName,
|
||||
&effectiveDockerImage,
|
||||
resourceRequirements,
|
||||
|
|
@ -1255,7 +1278,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
|
|||
}
|
||||
|
||||
if volumeClaimTemplate, err = generatePersistentVolumeClaimTemplate(spec.Volume.Size,
|
||||
spec.Volume.StorageClass); err != nil {
|
||||
spec.Volume.StorageClass, spec.Volume.Selector); err != nil {
|
||||
return nil, fmt.Errorf("could not generate volume claim template: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -1503,7 +1526,8 @@ func (c *Cluster) addAdditionalVolumes(podSpec *v1.PodSpec,
|
|||
podSpec.Volumes = volumes
|
||||
}
|
||||
|
||||
func generatePersistentVolumeClaimTemplate(volumeSize, volumeStorageClass string) (*v1.PersistentVolumeClaim, error) {
|
||||
func generatePersistentVolumeClaimTemplate(volumeSize, volumeStorageClass string,
|
||||
volumeSelector *metav1.LabelSelector) (*v1.PersistentVolumeClaim, error) {
|
||||
|
||||
var storageClassName *string
|
||||
|
||||
|
|
@ -1536,6 +1560,7 @@ func generatePersistentVolumeClaimTemplate(volumeSize, volumeStorageClass string
|
|||
},
|
||||
StorageClassName: storageClassName,
|
||||
VolumeMode: &volumeMode,
|
||||
Selector: volumeSelector,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -1547,10 +1572,11 @@ func (c *Cluster) generateUserSecrets() map[string]*v1.Secret {
|
|||
namespace := c.Namespace
|
||||
for username, pgUser := range c.pgUsers {
|
||||
//Skip users with no password i.e. human users (they'll be authenticated using pam)
|
||||
secret := c.generateSingleUserSecret(namespace, pgUser)
|
||||
secret := c.generateSingleUserSecret(pgUser.Namespace, pgUser)
|
||||
if secret != nil {
|
||||
secrets[username] = secret
|
||||
}
|
||||
namespace = pgUser.Namespace
|
||||
}
|
||||
/* special case for the system user */
|
||||
for _, systemUser := range c.systemUsers {
|
||||
|
|
@ -1590,7 +1616,7 @@ func (c *Cluster) generateSingleUserSecret(namespace string, pgUser spec.PgUser)
|
|||
secret := v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: c.credentialSecretName(username),
|
||||
Namespace: namespace,
|
||||
Namespace: pgUser.Namespace,
|
||||
Labels: lbls,
|
||||
Annotations: c.annotationsSet(nil),
|
||||
},
|
||||
|
|
@ -1785,6 +1811,14 @@ func (c *Cluster) generateCloneEnvironment(description *acidv1.CloneDescription)
|
|||
},
|
||||
}
|
||||
result = append(result, envs...)
|
||||
} else if c.OpConfig.WALAZStorageAccount != "" {
|
||||
envs := []v1.EnvVar{
|
||||
{
|
||||
Name: "CLONE_AZURE_STORAGE_ACCOUNT",
|
||||
Value: c.OpConfig.WALAZStorageAccount,
|
||||
},
|
||||
}
|
||||
result = append(result, envs...)
|
||||
} else {
|
||||
c.logger.Error("Cannot figure out S3 or GS bucket. Both are empty.")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1207,6 +1207,12 @@ func TestSidecars(t *testing.T) {
|
|||
}
|
||||
|
||||
spec = acidv1.PostgresSpec{
|
||||
PostgresqlParam: acidv1.PostgresqlParam{
|
||||
PgVersion: "12.1",
|
||||
Parameters: map[string]string{
|
||||
"max_connections": "100",
|
||||
},
|
||||
},
|
||||
TeamID: "myapp", NumberOfInstances: 1,
|
||||
Resources: acidv1.Resources{
|
||||
ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
|
||||
|
|
@ -1503,3 +1509,106 @@ func TestGenerateCapabilities(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolumeSelector(t *testing.T) {
|
||||
testName := "TestVolumeSelector"
|
||||
makeSpec := func(volume acidv1.Volume) acidv1.PostgresSpec {
|
||||
return acidv1.PostgresSpec{
|
||||
TeamID: "myapp",
|
||||
NumberOfInstances: 0,
|
||||
Resources: acidv1.Resources{
|
||||
ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
|
||||
ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
|
||||
},
|
||||
Volume: volume,
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
subTest string
|
||||
volume acidv1.Volume
|
||||
wantSelector *metav1.LabelSelector
|
||||
}{
|
||||
{
|
||||
subTest: "PVC template has no selector",
|
||||
volume: acidv1.Volume{
|
||||
Size: "1G",
|
||||
},
|
||||
wantSelector: nil,
|
||||
},
|
||||
{
|
||||
subTest: "PVC template has simple label selector",
|
||||
volume: acidv1.Volume{
|
||||
Size: "1G",
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"environment": "unittest"},
|
||||
},
|
||||
},
|
||||
wantSelector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"environment": "unittest"},
|
||||
},
|
||||
},
|
||||
{
|
||||
subTest: "PVC template has full selector",
|
||||
volume: acidv1.Volume{
|
||||
Size: "1G",
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"environment": "unittest"},
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "flavour",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"banana", "chocolate"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantSelector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"environment": "unittest"},
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "flavour",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"banana", "chocolate"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cluster := New(
|
||||
Config{
|
||||
OpConfig: config.Config{
|
||||
PodManagementPolicy: "ordered_ready",
|
||||
ProtectedRoles: []string{"admin"},
|
||||
Auth: config.Auth{
|
||||
SuperUsername: superUserName,
|
||||
ReplicationUsername: replicationUserName,
|
||||
},
|
||||
},
|
||||
}, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder)
|
||||
|
||||
for _, tt := range tests {
|
||||
pgSpec := makeSpec(tt.volume)
|
||||
sts, err := cluster.generateStatefulSet(&pgSpec)
|
||||
if err != nil {
|
||||
t.Fatalf("%s %s: no statefulset created %v", testName, tt.subTest, err)
|
||||
}
|
||||
|
||||
volIdx := len(sts.Spec.VolumeClaimTemplates)
|
||||
for i, ct := range sts.Spec.VolumeClaimTemplates {
|
||||
if ct.ObjectMeta.Name == constants.DataVolumeName {
|
||||
volIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if volIdx == len(sts.Spec.VolumeClaimTemplates) {
|
||||
t.Errorf("%s %s: no datavolume found in sts", testName, tt.subTest)
|
||||
}
|
||||
|
||||
selector := sts.Spec.VolumeClaimTemplates[volIdx].Spec.Selector
|
||||
if !reflect.DeepEqual(selector, tt.wantSelector) {
|
||||
t.Errorf("%s %s: expected: %#v but got: %#v", testName, tt.subTest, tt.wantSelector, selector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -304,8 +304,19 @@ func (c *Cluster) MigrateMasterPod(podName spec.NamespacedName) error {
|
|||
}
|
||||
|
||||
masterCandidateName := util.NameFromMeta(masterCandidatePod.ObjectMeta)
|
||||
if err := c.Switchover(oldMaster, masterCandidateName); err != nil {
|
||||
return fmt.Errorf("could not failover to pod %q: %v", masterCandidateName, err)
|
||||
err = retryutil.Retry(1*time.Minute, 5*time.Minute,
|
||||
func() (bool, error) {
|
||||
err := c.Switchover(oldMaster, masterCandidateName)
|
||||
if err != nil {
|
||||
c.logger.Errorf("could not failover to pod %q: %v", masterCandidateName, err)
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not migrate master pod: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ func (c *Cluster) listResources() error {
|
|||
}
|
||||
|
||||
for _, obj := range c.Secrets {
|
||||
c.logger.Infof("found secret: %q (uid: %q)", util.NameFromMeta(obj.ObjectMeta), obj.UID)
|
||||
c.logger.Infof("found secret: %q (uid: %q) namesapce: %s", util.NameFromMeta(obj.ObjectMeta), obj.UID, obj.ObjectMeta.Namespace)
|
||||
}
|
||||
|
||||
for role, endpoint := range c.Endpoints {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ package cluster
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
|
||||
"github.com/zalando/postgres-operator/pkg/spec"
|
||||
|
|
@ -260,13 +263,18 @@ func (c *Cluster) syncPodDisruptionBudget(isUpdate bool) error {
|
|||
}
|
||||
|
||||
func (c *Cluster) syncStatefulSet() error {
|
||||
var (
|
||||
masterPod *v1.Pod
|
||||
postgresConfig map[string]interface{}
|
||||
instanceRestartRequired bool
|
||||
)
|
||||
|
||||
podsToRecreate := make([]v1.Pod, 0)
|
||||
switchoverCandidates := make([]spec.NamespacedName, 0)
|
||||
|
||||
pods, err := c.listPods()
|
||||
if err != nil {
|
||||
c.logger.Infof("could not list pods of the statefulset: %v", err)
|
||||
c.logger.Warnf("could not list pods of the statefulset: %v", err)
|
||||
}
|
||||
|
||||
// NB: Be careful to consider the codepath that acts on podsRollingUpdateRequired before returning early.
|
||||
|
|
@ -379,8 +387,48 @@ func (c *Cluster) syncStatefulSet() error {
|
|||
// Apply special PostgreSQL parameters that can only be set via the Patroni API.
|
||||
// it is important to do it after the statefulset pods are there, but before the rolling update
|
||||
// since those parameters require PostgreSQL restart.
|
||||
if err := c.checkAndSetGlobalPostgreSQLConfiguration(); err != nil {
|
||||
return fmt.Errorf("could not set cluster-wide PostgreSQL configuration options: %v", err)
|
||||
pods, err = c.listPods()
|
||||
if err != nil {
|
||||
c.logger.Warnf("could not get list of pods to apply special PostgreSQL parameters only to be set via Patroni API: %v", err)
|
||||
}
|
||||
|
||||
// get Postgres config, compare with manifest and update via Patroni PATCH endpoint if it differs
|
||||
// Patroni's config endpoint is just a "proxy" to DCS. It is enough to patch it only once and it doesn't matter which pod is used.
|
||||
for i, pod := range pods {
|
||||
podName := util.NameFromMeta(pods[i].ObjectMeta)
|
||||
config, err := c.patroni.GetConfig(&pod)
|
||||
if err != nil {
|
||||
c.logger.Warningf("could not get Postgres config from pod %s: %v", podName, err)
|
||||
continue
|
||||
}
|
||||
instanceRestartRequired, err = c.checkAndSetGlobalPostgreSQLConfiguration(&pod, config)
|
||||
if err != nil {
|
||||
c.logger.Warningf("could not set PostgreSQL configuration options for pod %s: %v", podName, err)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// if the config update requires a restart, call Patroni restart for replicas first, then master
|
||||
if instanceRestartRequired {
|
||||
c.logger.Debug("restarting Postgres server within pods")
|
||||
ttl, ok := postgresConfig["ttl"].(int32)
|
||||
if !ok {
|
||||
ttl = 30
|
||||
}
|
||||
for i, pod := range pods {
|
||||
role := PostgresRole(pod.Labels[c.OpConfig.PodRoleLabel])
|
||||
if role == Master {
|
||||
masterPod = &pods[i]
|
||||
continue
|
||||
}
|
||||
c.restartInstance(&pod)
|
||||
time.Sleep(time.Duration(ttl) * time.Second)
|
||||
}
|
||||
|
||||
if masterPod != nil {
|
||||
c.restartInstance(masterPod)
|
||||
}
|
||||
}
|
||||
|
||||
// if we get here we also need to re-create the pods (either leftovers from the old
|
||||
|
|
@ -396,6 +444,21 @@ func (c *Cluster) syncStatefulSet() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Cluster) restartInstance(pod *v1.Pod) {
|
||||
podName := util.NameFromMeta(pod.ObjectMeta)
|
||||
role := PostgresRole(pod.Labels[c.OpConfig.PodRoleLabel])
|
||||
|
||||
c.eventRecorder.Event(c.GetReference(), v1.EventTypeNormal, "Update", fmt.Sprintf("restarting Postgres server within %s pod %s", role, pod.Name))
|
||||
|
||||
if err := c.patroni.Restart(pod); err != nil {
|
||||
c.logger.Warningf("could not restart Postgres server within %s pod %s: %v", role, podName, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Debugf("Postgres server successfuly restarted in %s pod %s", role, podName)
|
||||
c.eventRecorder.Event(c.GetReference(), v1.EventTypeNormal, "Update", fmt.Sprintf("Postgres server restart done for %s pod %s", role, pod.Name))
|
||||
}
|
||||
|
||||
// AnnotationsToPropagate get the annotations to update if required
|
||||
// based on the annotations in postgres CRD
|
||||
func (c *Cluster) AnnotationsToPropagate(annotations map[string]string) map[string]string {
|
||||
|
|
@ -429,46 +492,77 @@ func (c *Cluster) AnnotationsToPropagate(annotations map[string]string) map[stri
|
|||
}
|
||||
|
||||
// checkAndSetGlobalPostgreSQLConfiguration checks whether cluster-wide API parameters
|
||||
// (like max_connections) has changed and if necessary sets it via the Patroni API
|
||||
func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration() error {
|
||||
var (
|
||||
err error
|
||||
pods []v1.Pod
|
||||
)
|
||||
// (like max_connections) have changed and if necessary sets it via the Patroni API
|
||||
func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration(pod *v1.Pod, patroniConfig map[string]interface{}) (bool, error) {
|
||||
configToSet := make(map[string]interface{})
|
||||
parametersToSet := make(map[string]string)
|
||||
effectivePgParameters := make(map[string]interface{})
|
||||
|
||||
// we need to extract those options from the cluster manifest.
|
||||
optionsToSet := make(map[string]string)
|
||||
pgOptions := c.Spec.Parameters
|
||||
// read effective Patroni config if set
|
||||
if patroniConfig != nil {
|
||||
effectivePostgresql := patroniConfig["postgresql"].(map[string]interface{})
|
||||
effectivePgParameters = effectivePostgresql[patroniPGParametersParameterName].(map[string]interface{})
|
||||
}
|
||||
|
||||
for k, v := range pgOptions {
|
||||
if isBootstrapOnlyParameter(k) {
|
||||
optionsToSet[k] = v
|
||||
// compare parameters under postgresql section with c.Spec.Postgresql.Parameters from manifest
|
||||
desiredPgParameters := c.Spec.Parameters
|
||||
for desiredOption, desiredValue := range desiredPgParameters {
|
||||
effectiveValue := effectivePgParameters[desiredOption]
|
||||
if isBootstrapOnlyParameter(desiredOption) && (effectiveValue != desiredValue) {
|
||||
parametersToSet[desiredOption] = desiredValue
|
||||
}
|
||||
}
|
||||
|
||||
if len(optionsToSet) == 0 {
|
||||
return nil
|
||||
if len(parametersToSet) > 0 {
|
||||
configToSet["postgresql"] = map[string]interface{}{patroniPGParametersParameterName: parametersToSet}
|
||||
}
|
||||
|
||||
if pods, err = c.listPods(); err != nil {
|
||||
return err
|
||||
// compare other options from config with c.Spec.Patroni from manifest
|
||||
desiredPatroniConfig := c.Spec.Patroni
|
||||
if desiredPatroniConfig.LoopWait > 0 && desiredPatroniConfig.LoopWait != uint32(patroniConfig["loop_wait"].(float64)) {
|
||||
configToSet["loop_wait"] = desiredPatroniConfig.LoopWait
|
||||
}
|
||||
if len(pods) == 0 {
|
||||
return fmt.Errorf("could not call Patroni API: cluster has no pods")
|
||||
if desiredPatroniConfig.MaximumLagOnFailover > 0 && desiredPatroniConfig.MaximumLagOnFailover != float32(patroniConfig["maximum_lag_on_failover"].(float64)) {
|
||||
configToSet["maximum_lag_on_failover"] = desiredPatroniConfig.MaximumLagOnFailover
|
||||
}
|
||||
if desiredPatroniConfig.PgHba != nil && !reflect.DeepEqual(desiredPatroniConfig.PgHba, (patroniConfig["pg_hba"])) {
|
||||
configToSet["pg_hba"] = desiredPatroniConfig.PgHba
|
||||
}
|
||||
if desiredPatroniConfig.RetryTimeout > 0 && desiredPatroniConfig.RetryTimeout != uint32(patroniConfig["retry_timeout"].(float64)) {
|
||||
configToSet["retry_timeout"] = desiredPatroniConfig.RetryTimeout
|
||||
}
|
||||
if desiredPatroniConfig.Slots != nil && !reflect.DeepEqual(desiredPatroniConfig.Slots, patroniConfig["slots"]) {
|
||||
configToSet["slots"] = desiredPatroniConfig.Slots
|
||||
}
|
||||
if desiredPatroniConfig.SynchronousMode != patroniConfig["synchronous_mode"] {
|
||||
configToSet["synchronous_mode"] = desiredPatroniConfig.SynchronousMode
|
||||
}
|
||||
if desiredPatroniConfig.SynchronousModeStrict != patroniConfig["synchronous_mode_strict"] {
|
||||
configToSet["synchronous_mode_strict"] = desiredPatroniConfig.SynchronousModeStrict
|
||||
}
|
||||
if desiredPatroniConfig.TTL > 0 && desiredPatroniConfig.TTL != uint32(patroniConfig["ttl"].(float64)) {
|
||||
configToSet["ttl"] = desiredPatroniConfig.TTL
|
||||
}
|
||||
|
||||
if len(configToSet) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
configToSetJson, err := json.Marshal(configToSet)
|
||||
if err != nil {
|
||||
c.logger.Debugf("could not convert config patch to JSON: %v", err)
|
||||
}
|
||||
|
||||
// try all pods until the first one that is successful, as it doesn't matter which pod
|
||||
// carries the request to change configuration through
|
||||
for _, pod := range pods {
|
||||
podName := util.NameFromMeta(pod.ObjectMeta)
|
||||
c.logger.Debugf("calling Patroni API on a pod %s to set the following Postgres options: %v",
|
||||
podName, optionsToSet)
|
||||
if err = c.patroni.SetPostgresParameters(&pod, optionsToSet); err == nil {
|
||||
return nil
|
||||
}
|
||||
c.logger.Warningf("could not patch postgres parameters with a pod %s: %v", podName, err)
|
||||
podName := util.NameFromMeta(pod.ObjectMeta)
|
||||
c.logger.Debugf("patching Postgres config via Patroni API on pod %s with following options: %s",
|
||||
podName, configToSetJson)
|
||||
if err = c.patroni.SetConfig(pod, configToSet); err != nil {
|
||||
return true, fmt.Errorf("could not patch postgres parameters with a pod %s: %v", podName, err)
|
||||
}
|
||||
return fmt.Errorf("could not reach Patroni API to set Postgres options: failed on every pod (%d total)",
|
||||
len(pods))
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *Cluster) syncSecrets() error {
|
||||
|
|
@ -483,7 +577,7 @@ func (c *Cluster) syncSecrets() error {
|
|||
for secretUsername, secretSpec := range secrets {
|
||||
if secret, err = c.KubeClient.Secrets(secretSpec.Namespace).Create(context.TODO(), secretSpec, metav1.CreateOptions{}); err == nil {
|
||||
c.Secrets[secret.UID] = secret
|
||||
c.logger.Debugf("created new secret %s, uid: %s", util.NameFromMeta(secret.ObjectMeta), secret.UID)
|
||||
c.logger.Debugf("created new secret %s, namespace: %s, uid: %s", util.NameFromMeta(secret.ObjectMeta), secretSpec.Namespace, secret.UID)
|
||||
continue
|
||||
}
|
||||
if k8sutil.ResourceAlreadyExists(err) {
|
||||
|
|
@ -521,7 +615,7 @@ func (c *Cluster) syncSecrets() error {
|
|||
userMap[secretUsername] = pwdUser
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("could not create secret for user %s: %v", secretUsername, err)
|
||||
return fmt.Errorf("could not create secret for user %s: in namespace %s: %v", secretUsername, secretSpec.Namespace, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -556,11 +650,12 @@ func (c *Cluster) syncRoles() (err error) {
|
|||
|
||||
// create list of database roles to query
|
||||
for _, u := range c.pgUsers {
|
||||
userNames = append(userNames, u.Name)
|
||||
pgRole := u.Name
|
||||
userNames = append(userNames, pgRole)
|
||||
// add team member role name with rename suffix in case we need to rename it back
|
||||
if u.Origin == spec.RoleOriginTeamsAPI && c.OpConfig.EnableTeamMemberDeprecation {
|
||||
deletedUsers[u.Name+c.OpConfig.RoleDeletionSuffix] = u.Name
|
||||
userNames = append(userNames, u.Name+c.OpConfig.RoleDeletionSuffix)
|
||||
deletedUsers[pgRole+c.OpConfig.RoleDeletionSuffix] = pgRole
|
||||
userNames = append(userNames, pgRole+c.OpConfig.RoleDeletionSuffix)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -663,13 +758,25 @@ func (c *Cluster) syncDatabases() error {
|
|||
}
|
||||
}
|
||||
|
||||
if len(createDatabases) > 0 {
|
||||
// trigger creation of pooler objects in new database in syncConnectionPooler
|
||||
if c.ConnectionPooler != nil {
|
||||
for _, role := range [2]PostgresRole{Master, Replica} {
|
||||
c.ConnectionPooler[role].LookupFunction = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set default privileges for prepared database
|
||||
for _, preparedDatabase := range preparedDatabases {
|
||||
if err := c.initDbConnWithName(preparedDatabase); err != nil {
|
||||
return fmt.Errorf("could not init database connection to %s", preparedDatabase)
|
||||
}
|
||||
if err = c.execAlterGlobalDefaultPrivileges(preparedDatabase+constants.OwnerRoleNameSuffix, preparedDatabase); err != nil {
|
||||
return err
|
||||
|
||||
for _, owner := range c.getOwnerRoles(preparedDatabase, c.Spec.PreparedDatabases[preparedDatabase].DefaultUsers) {
|
||||
if err = c.execAlterGlobalDefaultPrivileges(owner, preparedDatabase); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ type ClusterStatus struct {
|
|||
|
||||
type TemplateParams map[string]interface{}
|
||||
|
||||
type InstallFunction func(schema string, user string, role PostgresRole) error
|
||||
type InstallFunction func(schema string, user string) error
|
||||
|
||||
type SyncReason []string
|
||||
|
||||
|
|
|
|||
|
|
@ -96,13 +96,13 @@ func (c *Cluster) syncUnderlyingEBSVolume() error {
|
|||
var modifySize *int64
|
||||
var modifyType *string
|
||||
|
||||
if targetValue.Iops != nil {
|
||||
if targetValue.Iops != nil && *targetValue.Iops >= int64(3000) {
|
||||
if volume.Iops != *targetValue.Iops {
|
||||
modifyIops = targetValue.Iops
|
||||
}
|
||||
}
|
||||
|
||||
if targetValue.Throughput != nil {
|
||||
if targetValue.Throughput != nil && *targetValue.Throughput >= int64(125) {
|
||||
if volume.Throughput != *targetValue.Throughput {
|
||||
modifyThroughput = targetValue.Throughput
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
|
|||
result.EnableSpiloWalPathCompat = fromCRD.EnableSpiloWalPathCompat
|
||||
result.EtcdHost = fromCRD.EtcdHost
|
||||
result.KubernetesUseConfigMaps = fromCRD.KubernetesUseConfigMaps
|
||||
result.DockerImage = util.Coalesce(fromCRD.DockerImage, "registry.opensource.zalan.do/acid/spilo-13:2.0-p7")
|
||||
result.DockerImage = util.Coalesce(fromCRD.DockerImage, "registry.opensource.zalan.do/acid/spilo-13:2.1-p1")
|
||||
result.Workers = util.CoalesceUInt32(fromCRD.Workers, 8)
|
||||
result.MinInstances = fromCRD.MinInstances
|
||||
result.MaxInstances = fromCRD.MaxInstances
|
||||
|
|
@ -82,6 +82,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
|
|||
result.EnableSidecars = util.CoalesceBool(fromCRD.Kubernetes.EnableSidecars, util.True())
|
||||
result.SecretNameTemplate = fromCRD.Kubernetes.SecretNameTemplate
|
||||
result.OAuthTokenSecretName = fromCRD.Kubernetes.OAuthTokenSecretName
|
||||
result.EnableCrossNamespaceSecret = fromCRD.Kubernetes.EnableCrossNamespaceSecret
|
||||
|
||||
result.InfrastructureRolesSecretName = fromCRD.Kubernetes.InfrastructureRolesSecretName
|
||||
if fromCRD.Kubernetes.InfrastructureRolesDefs != nil {
|
||||
|
|
@ -145,6 +146,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
|
|||
result.KubeIAMRole = fromCRD.AWSGCP.KubeIAMRole
|
||||
result.WALGSBucket = fromCRD.AWSGCP.WALGSBucket
|
||||
result.GCPCredentials = fromCRD.AWSGCP.GCPCredentials
|
||||
result.WALAZStorageAccount = fromCRD.AWSGCP.WALAZStorageAccount
|
||||
result.AdditionalSecretMount = fromCRD.AWSGCP.AdditionalSecretMount
|
||||
result.AdditionalSecretMountPath = util.Coalesce(fromCRD.AWSGCP.AdditionalSecretMountPath, "/meta/credentials")
|
||||
result.EnableEBSGp3Migration = fromCRD.AWSGCP.EnableEBSGp3Migration
|
||||
|
|
@ -152,7 +154,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
|
|||
|
||||
// logical backup config
|
||||
result.LogicalBackupSchedule = util.Coalesce(fromCRD.LogicalBackup.Schedule, "30 00 * * *")
|
||||
result.LogicalBackupDockerImage = util.Coalesce(fromCRD.LogicalBackup.DockerImage, "registry.opensource.zalan.do/acid/logical-backup:v1.6.3")
|
||||
result.LogicalBackupDockerImage = util.Coalesce(fromCRD.LogicalBackup.DockerImage, "registry.opensource.zalan.do/acid/logical-backup:v1.7.0")
|
||||
result.LogicalBackupProvider = util.Coalesce(fromCRD.LogicalBackup.BackupProvider, "s3")
|
||||
result.LogicalBackupS3Bucket = fromCRD.LogicalBackup.S3Bucket
|
||||
result.LogicalBackupS3Region = fromCRD.LogicalBackup.S3Region
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ const (
|
|||
type PgUser struct {
|
||||
Origin RoleOrigin `yaml:"-"`
|
||||
Name string `yaml:"-"`
|
||||
Namespace string `yaml:"-"`
|
||||
Password string `yaml:"-"`
|
||||
Flags []string `yaml:"user_flags"`
|
||||
MemberOf []string `yaml:"inrole"`
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ type Scalyr struct {
|
|||
// LogicalBackup defines configuration for logical backup
|
||||
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:v1.6.3"`
|
||||
LogicalBackupDockerImage string `name:"logical_backup_docker_image" default:"registry.opensource.zalan.do/acid/logical-backup:v1.7.0"`
|
||||
LogicalBackupProvider string `name:"logical_backup_provider" default:"s3"`
|
||||
LogicalBackupS3Bucket string `name:"logical_backup_s3_bucket" default:""`
|
||||
LogicalBackupS3Region string `name:"logical_backup_s3_region" default:""`
|
||||
|
|
@ -152,7 +152,7 @@ type Config struct {
|
|||
WatchedNamespace string `name:"watched_namespace"` // special values: "*" means 'watch all namespaces', the empty string "" means 'watch a namespace where operator is deployed to'
|
||||
KubernetesUseConfigMaps bool `name:"kubernetes_use_configmaps" default:"false"`
|
||||
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-13:2.0-p7"`
|
||||
DockerImage string `name:"docker_image" default:"registry.opensource.zalan.do/acid/spilo-13:2.1-p1"`
|
||||
SidecarImages map[string]string `name:"sidecar_docker_images"` // deprecated in favour of SidecarContainers
|
||||
SidecarContainers []v1.Container `name:"sidecars"`
|
||||
PodServiceAccountName string `name:"pod_service_account_name" default:"postgres-pod"`
|
||||
|
|
@ -167,6 +167,7 @@ type Config struct {
|
|||
KubeIAMRole string `name:"kube_iam_role"`
|
||||
WALGSBucket string `name:"wal_gs_bucket"`
|
||||
GCPCredentials string `name:"gcp_credentials"`
|
||||
WALAZStorageAccount string `name:"wal_az_storage_account"`
|
||||
AdditionalSecretMount string `name:"additional_secret_mount"`
|
||||
AdditionalSecretMountPath string `name:"additional_secret_mount_path" default:"/meta/credentials"`
|
||||
EnableEBSGp3Migration bool `name:"enable_ebs_gp3_migration" default:"false"`
|
||||
|
|
@ -207,6 +208,7 @@ type Config struct {
|
|||
PostgresSuperuserTeams []string `name:"postgres_superuser_teams" default:""`
|
||||
SetMemoryRequestToLimit bool `name:"set_memory_request_to_limit" default:"false"`
|
||||
EnableLazySpiloUpgrade bool `name:"enable_lazy_spilo_upgrade" default:"false"`
|
||||
EnableCrossNamespaceSecret bool `name:"enable_cross_namespace_secret" default:"false"`
|
||||
EnablePgVersionEnvVar bool `name:"enable_pgversion_env_var" default:"true"`
|
||||
EnableSpiloWalPathCompat bool `name:"enable_spilo_wal_path_compat" default:"false"`
|
||||
MajorVersionUpgradeMode string `name:"major_version_upgrade_mode" default:"off"`
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import (
|
|||
const (
|
||||
failoverPath = "/failover"
|
||||
configPath = "/config"
|
||||
statusPath = "/patroni"
|
||||
restartPath = "/restart"
|
||||
apiPort = 8008
|
||||
timeout = 30 * time.Second
|
||||
)
|
||||
|
|
@ -28,6 +30,9 @@ type Interface interface {
|
|||
Switchover(master *v1.Pod, candidate string) error
|
||||
SetPostgresParameters(server *v1.Pod, options map[string]string) error
|
||||
GetMemberData(server *v1.Pod) (MemberData, error)
|
||||
Restart(server *v1.Pod) error
|
||||
GetConfig(server *v1.Pod) (map[string]interface{}, error)
|
||||
SetConfig(server *v1.Pod, config map[string]interface{}) error
|
||||
}
|
||||
|
||||
// Patroni API client
|
||||
|
|
@ -103,6 +108,32 @@ func (p *Patroni) httpPostOrPatch(method string, url string, body *bytes.Buffer)
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *Patroni) httpGet(url string) (string, error) {
|
||||
request, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not create request: %v", err)
|
||||
}
|
||||
|
||||
p.logger.Debugf("making GET http request: %s", request.URL.String())
|
||||
|
||||
resp, err := p.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not make request: %v", err)
|
||||
}
|
||||
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read response: %v", err)
|
||||
}
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
return "", fmt.Errorf("could not close request: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return string(bodyBytes), fmt.Errorf("patroni returned '%d'", resp.StatusCode)
|
||||
}
|
||||
return string(bodyBytes), nil
|
||||
}
|
||||
|
||||
// Switchover by calling Patroni REST API
|
||||
func (p *Patroni) Switchover(master *v1.Pod, candidate string) error {
|
||||
buf := &bytes.Buffer{}
|
||||
|
|
@ -133,6 +164,20 @@ func (p *Patroni) SetPostgresParameters(server *v1.Pod, parameters map[string]st
|
|||
return p.httpPostOrPatch(http.MethodPatch, apiURLString+configPath, buf)
|
||||
}
|
||||
|
||||
//SetConfig sets Patroni options via Patroni patch API call.
|
||||
func (p *Patroni) SetConfig(server *v1.Pod, config map[string]interface{}) error {
|
||||
buf := &bytes.Buffer{}
|
||||
err := json.NewEncoder(buf).Encode(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not encode json: %v", err)
|
||||
}
|
||||
apiURLString, err := apiURL(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.httpPostOrPatch(http.MethodPatch, apiURLString+configPath, buf)
|
||||
}
|
||||
|
||||
// MemberDataPatroni child element
|
||||
type MemberDataPatroni struct {
|
||||
Version string `json:"version"`
|
||||
|
|
@ -149,6 +194,48 @@ type MemberData struct {
|
|||
Patroni MemberDataPatroni `json:"patroni"`
|
||||
}
|
||||
|
||||
func (p *Patroni) GetConfigOrStatus(server *v1.Pod, path string) (map[string]interface{}, error) {
|
||||
result := make(map[string]interface{})
|
||||
apiURLString, err := apiURL(server)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
body, err := p.httpGet(apiURLString + path)
|
||||
err = json.Unmarshal([]byte(body), &result)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (p *Patroni) GetStatus(server *v1.Pod) (map[string]interface{}, error) {
|
||||
return p.GetConfigOrStatus(server, statusPath)
|
||||
}
|
||||
|
||||
func (p *Patroni) GetConfig(server *v1.Pod) (map[string]interface{}, error) {
|
||||
return p.GetConfigOrStatus(server, configPath)
|
||||
}
|
||||
|
||||
//Restart method restarts instance via Patroni POST API call.
|
||||
func (p *Patroni) Restart(server *v1.Pod) error {
|
||||
buf := &bytes.Buffer{}
|
||||
err := json.NewEncoder(buf).Encode(map[string]interface{}{"restart_pending": true})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not encode json: %v", err)
|
||||
}
|
||||
apiURLString, err := apiURL(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status, err := p.GetStatus(server)
|
||||
pending_restart, ok := status["pending_restart"]
|
||||
if !ok || !pending_restart.(bool) {
|
||||
return nil
|
||||
}
|
||||
return p.httpPostOrPatch(http.MethodPost, apiURLString+restartPath, buf)
|
||||
}
|
||||
|
||||
// GetMemberData read member data from patroni API
|
||||
func (p *Patroni) GetMemberData(server *v1.Pod) (MemberData, error) {
|
||||
|
||||
|
|
|
|||
|
|
@ -71,3 +71,25 @@ spec:
|
|||
"11"
|
||||
]
|
||||
}
|
||||
# Exemple of settings to make snapshot view working in the ui when using AWS
|
||||
# - name: WALE_S3_ENDPOINT
|
||||
# value: https+path://s3.us-east-1.amazonaws.com:443
|
||||
# - name: SPILO_S3_BACKUP_PREFIX
|
||||
# value: spilo/
|
||||
# - name: AWS_ACCESS_KEY_ID
|
||||
# valueFrom:
|
||||
# secretKeyRef:
|
||||
# name: <postgres operator secret with AWS token>
|
||||
# key: AWS_ACCESS_KEY_ID
|
||||
# - name: AWS_SECRET_ACCESS_KEY
|
||||
# valueFrom:
|
||||
# secretKeyRef:
|
||||
# name: <postgres operator secret with AWS token>
|
||||
# key: AWS_SECRET_ACCESS_KEY
|
||||
# - name: AWS_DEFAULT_REGION
|
||||
# valueFrom:
|
||||
# secretKeyRef:
|
||||
# name: <postgres operator secret with AWS token>
|
||||
# key: AWS_DEFAULT_REGION
|
||||
# - name: SPILO_S3_BACKUP_BUCKET
|
||||
# value: <s3 bucket used by the operator>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
apiVersion: "networking.k8s.io/v1beta1"
|
||||
apiVersion: "networking.k8s.io/v1"
|
||||
kind: "Ingress"
|
||||
metadata:
|
||||
name: "postgres-operator-ui"
|
||||
|
|
@ -10,6 +10,10 @@ spec:
|
|||
- host: "ui.example.org"
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: "postgres-operator-ui"
|
||||
servicePort: 80
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
backend:
|
||||
service:
|
||||
name: "postgres-operator-ui"
|
||||
port:
|
||||
number: 80
|
||||
|
|
|
|||
Loading…
Reference in New Issue