diff --git a/.github/ISSUE_TEMPLATE/postgres-operator-issue-template.md b/.github/ISSUE_TEMPLATE/postgres-operator-issue-template.md index 3a0e52bf5..ee3a704ea 100644 --- a/.github/ISSUE_TEMPLATE/postgres-operator-issue-template.md +++ b/.github/ISSUE_TEMPLATE/postgres-operator-issue-template.md @@ -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. ghcr.io/zalando/postgres-operator:v1.12.2 +- **Which image of the operator are you using?** e.g. ghcr.io/zalando/postgres-operator:v1.13.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.] diff --git a/.github/workflows/publish_ghcr_image.yaml b/.github/workflows/publish_ghcr_image.yaml index a36e9757e..7633ccc3c 100644 --- a/.github/workflows/publish_ghcr_image.yaml +++ b/.github/workflows/publish_ghcr_image.yaml @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-go@v2 with: - go-version: "^1.22.3" + go-version: "^1.22.5" - name: Run unit tests run: make deps mocks test diff --git a/.github/workflows/run_e2e.yaml b/.github/workflows/run_e2e.yaml index 9da8a1c08..df83a31c4 100644 --- a/.github/workflows/run_e2e.yaml +++ b/.github/workflows/run_e2e.yaml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v1 - uses: actions/setup-go@v2 with: - go-version: "^1.22.3" + go-version: "^1.22.5" - name: Make dependencies run: make deps mocks - name: Code generation diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 3b075d041..c0e731e5e 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: - go-version: "^1.22.3" + go-version: "^1.22.5" - name: Make dependencies run: make deps mocks - name: Compile diff --git a/Makefile b/Makefile index e8b8e2b74..3b7ae4ede 100644 --- a/Makefile +++ b/Makefile @@ -69,7 +69,7 @@ docker: ${DOCKERDIR}/${DOCKERFILE} docker build --rm -t "$(IMAGE):$(TAG)$(CDP_TAG)$(DEBUG_FRESH)$(DEBUG_POSTFIX)" -f "${DOCKERDIR}/${DOCKERFILE}" --build-arg VERSION="${VERSION}" . indocker-race: - docker run --rm -v "${GOPATH}":"${GOPATH}" -e GOPATH="${GOPATH}" -e RACE=1 -w ${PWD} golang:1.22.3 bash -c "make linux" + docker run --rm -v "${GOPATH}":"${GOPATH}" -e GOPATH="${GOPATH}" -e RACE=1 -w ${PWD} golang:1.22.5 bash -c "make linux" push: docker push "$(IMAGE):$(TAG)$(CDP_TAG)" @@ -78,7 +78,7 @@ mocks: GO111MODULE=on go generate ./... tools: - GO111MODULE=on go get -d k8s.io/client-go@kubernetes-1.28.10 + GO111MODULE=on go get -d k8s.io/client-go@kubernetes-1.28.12 GO111MODULE=on go install github.com/golang/mock/mockgen@v1.6.0 GO111MODULE=on go mod tidy diff --git a/README.md b/README.md index 0b8f10c91..c34bc6f6f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ pipelines with no access to Kubernetes API directly, promoting infrastructure as ### PostgreSQL features -* Supports PostgreSQL 16, starting from 11+ +* Supports PostgreSQL 16, starting from 12+ * Streaming replication cluster via Patroni * Point-In-Time-Recovery with [pg_basebackup](https://www.postgresql.org/docs/16/app-pgbasebackup.html) / @@ -57,13 +57,13 @@ production for over five years. | Release | Postgres versions | K8s versions | Golang | | :-------- | :---------------: | :---------------: | :-----: | -| v1.12.2 | 11 → 16 | 1.27+ | 1.22.3 | +| v1.13.0 | 12 → 16 | 1.27+ | 1.22.5 | +| v1.12.0 | 11 → 16 | 1.27+ | 1.22.3 | | v1.11.0 | 11 → 16 | 1.27+ | 1.21.7 | | v1.10.1 | 10 → 15 | 1.21+ | 1.19.8 | | v1.9.0 | 10 → 15 | 1.21+ | 1.18.9 | | v1.8.2 | 9.5 → 14 | 1.20 → 1.24 | 1.17.4 | - ## Getting started For a quick first impression follow the instructions of this diff --git a/charts/postgres-operator-ui/Chart.yaml b/charts/postgres-operator-ui/Chart.yaml index 1bb27c741..1d5597940 100644 --- a/charts/postgres-operator-ui/Chart.yaml +++ b/charts/postgres-operator-ui/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: postgres-operator-ui -version: 1.12.2 -appVersion: 1.12.2 +version: 1.13.0 +appVersion: 1.13.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: diff --git a/charts/postgres-operator-ui/index.yaml b/charts/postgres-operator-ui/index.yaml index 90e3a7f0a..1b89eeb60 100644 --- a/charts/postgres-operator-ui/index.yaml +++ b/charts/postgres-operator-ui/index.yaml @@ -1,9 +1,32 @@ apiVersion: v1 entries: postgres-operator-ui: + - apiVersion: v2 + appVersion: 1.13.0 + created: "2024-08-21T18:55:36.524305158+02:00" + description: Postgres Operator UI provides a graphical interface for a convenient + database-as-a-service user experience + digest: e0444e516b50f82002d1a733527813c51759a627cefdd1005cea73659f824ea8 + 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.13.0.tgz + version: 1.13.0 - apiVersion: v2 appVersion: 1.12.2 - created: "2024-06-14T10:31:52.852963015+02:00" + created: "2024-08-21T18:55:36.521875733+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: cbcef400c23ccece27d97369ad629278265c013e0a45c0b7f33e7568a082fedd @@ -26,7 +49,7 @@ entries: version: 1.12.2 - apiVersion: v2 appVersion: 1.11.0 - created: "2024-06-14T10:31:52.849576888+02:00" + created: "2024-08-21T18:55:36.51959105+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: a45f2284045c2a9a79750a36997386444f39b01ac722b17c84b431457577a3a2 @@ -49,7 +72,7 @@ entries: version: 1.11.0 - apiVersion: v2 appVersion: 1.10.1 - created: "2024-06-14T10:31:52.843219526+02:00" + created: "2024-08-21T18:55:36.516518177+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: 2e5e7a82aebee519ec57c6243eb8735124aa4585a3a19c66ffd69638fbeb11ce @@ -72,7 +95,7 @@ entries: version: 1.10.1 - apiVersion: v2 appVersion: 1.9.0 - created: "2024-06-14T10:31:52.857573553+02:00" + created: "2024-08-21T18:55:36.52712908+02:00" description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience digest: df434af6c8b697fe0631017ecc25e3c79e125361ae6622347cea41a545153bdc @@ -93,27 +116,4 @@ entries: urls: - postgres-operator-ui-1.9.0.tgz version: 1.9.0 - - apiVersion: v2 - appVersion: 1.8.2 - created: "2024-06-14T10:31:52.855335455+02:00" - description: Postgres Operator UI provides a graphical interface for a convenient - database-as-a-service user experience - digest: fbfc90fa8fd007a08a7c02e0ec9108bb8282cbb42b8c976d88f2193d6edff30c - 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.8.2.tgz - version: 1.8.2 -generated: "2024-06-14T10:31:52.839113675+02:00" +generated: "2024-08-21T18:55:36.512456099+02:00" diff --git a/charts/postgres-operator-ui/postgres-operator-ui-1.13.0.tgz b/charts/postgres-operator-ui/postgres-operator-ui-1.13.0.tgz new file mode 100644 index 000000000..21aadc076 Binary files /dev/null and b/charts/postgres-operator-ui/postgres-operator-ui-1.13.0.tgz differ diff --git a/charts/postgres-operator-ui/postgres-operator-ui-1.8.2.tgz b/charts/postgres-operator-ui/postgres-operator-ui-1.8.2.tgz deleted file mode 100644 index 806ee9b23..000000000 Binary files a/charts/postgres-operator-ui/postgres-operator-ui-1.8.2.tgz and /dev/null differ diff --git a/charts/postgres-operator-ui/values.yaml b/charts/postgres-operator-ui/values.yaml index 2342496c5..22f787826 100644 --- a/charts/postgres-operator-ui/values.yaml +++ b/charts/postgres-operator-ui/values.yaml @@ -8,7 +8,7 @@ replicaCount: 1 image: registry: ghcr.io repository: zalando/postgres-operator-ui - tag: v1.12.2 + tag: v1.13.0 pullPolicy: "IfNotPresent" # Optionally specify an array of imagePullSecrets. diff --git a/charts/postgres-operator/Chart.yaml b/charts/postgres-operator/Chart.yaml index 38c2e6da8..ae4723b0e 100644 --- a/charts/postgres-operator/Chart.yaml +++ b/charts/postgres-operator/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: postgres-operator -version: 1.12.2 -appVersion: 1.12.2 +version: 1.13.0 +appVersion: 1.13.0 home: https://github.com/zalando/postgres-operator description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes keywords: diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index bf4ae34b1..5edb7044f 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -68,7 +68,7 @@ spec: type: string docker_image: type: string - default: "ghcr.io/zalando/spilo-16:3.2-p3" + default: "ghcr.io/zalando/spilo-16:3.3-p1" enable_crd_registration: type: boolean default: true @@ -160,7 +160,7 @@ spec: properties: major_version_upgrade_mode: type: string - default: "off" + default: "manual" major_version_upgrade_team_allow_list: type: array items: @@ -211,9 +211,9 @@ spec: enable_init_containers: type: boolean default: true - enable_secrets_deletion: + enable_owner_references: type: boolean - default: true + default: false enable_persistent_volume_claim_deletion: type: boolean default: true @@ -226,6 +226,9 @@ spec: enable_readiness_probe: type: boolean default: false + enable_secrets_deletion: + type: boolean + default: true enable_sidecars: type: boolean default: true @@ -469,7 +472,6 @@ spec: type: string additional_secret_mount_path: type: string - default: "/meta/credentials" aws_region: type: string default: "eu-central-1" @@ -508,7 +510,7 @@ spec: pattern: '^(\d+m|\d+(\.\d{1,3})?)$' logical_backup_docker_image: type: string - default: "ghcr.io/zalando/postgres-operator/logical-backup:v1.12.2" + default: "ghcr.io/zalando/postgres-operator/logical-backup:v1.13.0" logical_backup_google_application_credentials: type: string logical_backup_job_prefix: diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index 0498625f2..ebaf2d1f8 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -226,7 +226,7 @@ spec: type: array 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))\ *$' + pattern: '^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ *$' masterServiceAnnotations: type: object additionalProperties: @@ -375,7 +375,6 @@ spec: version: type: string enum: - - "11" - "12" - "13" - "14" diff --git a/charts/postgres-operator/index.yaml b/charts/postgres-operator/index.yaml index 63a9d732c..c72604daa 100644 --- a/charts/postgres-operator/index.yaml +++ b/charts/postgres-operator/index.yaml @@ -1,9 +1,31 @@ apiVersion: v1 entries: postgres-operator: + - apiVersion: v2 + appVersion: 1.13.0 + created: "2024-08-21T18:54:43.160735116+02:00" + description: Postgres Operator creates and manages PostgreSQL clusters running + in Kubernetes + digest: a839601689aea0a7e6bc0712a5244d435683cf3314c95794097ff08540e1dfef + 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.13.0.tgz + version: 1.13.0 - apiVersion: v2 appVersion: 1.12.2 - created: "2024-06-14T10:30:44.071387784+02:00" + created: "2024-08-21T18:54:43.152249286+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 65858d14a40d7fd90c32bd9fc60021acc9555c161079f43a365c70171eaf21d8 @@ -25,7 +47,7 @@ entries: version: 1.12.2 - apiVersion: v2 appVersion: 1.11.0 - created: "2024-06-14T10:30:44.065353504+02:00" + created: "2024-08-21T18:54:43.145837894+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 3914b5e117bda0834f05c9207f007e2ac372864cf6e86dcc2e1362bbe46c14d9 @@ -47,7 +69,7 @@ entries: version: 1.11.0 - apiVersion: v2 appVersion: 1.10.1 - created: "2024-06-14T10:30:44.059080224+02:00" + created: "2024-08-21T18:54:43.139552116+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: cc3baa41753da92466223d0b334df27e79c882296577b404a8e9071411fcf19c @@ -69,7 +91,7 @@ entries: version: 1.10.1 - apiVersion: v2 appVersion: 1.9.0 - created: "2024-06-14T10:30:44.084760658+02:00" + created: "2024-08-21T18:54:43.168490032+02:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: 64df90c898ca591eb3a330328173ffaadfbf9ddd474d8c42ed143edc9e3f4276 @@ -89,26 +111,4 @@ entries: urls: - postgres-operator-1.9.0.tgz version: 1.9.0 - - apiVersion: v2 - appVersion: 1.8.2 - created: "2024-06-14T10:30:44.077744166+02:00" - description: Postgres Operator creates and manages PostgreSQL clusters running - in Kubernetes - digest: f77ffad2e98b72a621e5527015cf607935d3ed688f10ba4b626435acb9631b5b - 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.8.2.tgz - version: 1.8.2 -generated: "2024-06-14T10:30:44.052436544+02:00" +generated: "2024-08-21T18:54:43.126871802+02:00" diff --git a/charts/postgres-operator/postgres-operator-1.13.0.tgz b/charts/postgres-operator/postgres-operator-1.13.0.tgz new file mode 100644 index 000000000..3d7ca4ce6 Binary files /dev/null and b/charts/postgres-operator/postgres-operator-1.13.0.tgz differ diff --git a/charts/postgres-operator/postgres-operator-1.8.2.tgz b/charts/postgres-operator/postgres-operator-1.8.2.tgz deleted file mode 100644 index 61f0f8aee..000000000 Binary files a/charts/postgres-operator/postgres-operator-1.8.2.tgz and /dev/null differ diff --git a/charts/postgres-operator/templates/clusterrole.yaml b/charts/postgres-operator/templates/clusterrole.yaml index 199086acc..d88affa0d 100644 --- a/charts/postgres-operator/templates/clusterrole.yaml +++ b/charts/postgres-operator/templates/clusterrole.yaml @@ -120,6 +120,7 @@ rules: - create - delete - get + - patch - update # to check nodes for node readiness label - apiGroups: @@ -196,6 +197,7 @@ rules: - get - list - patch + - update # to CRUD cron jobs for logical backups - apiGroups: - batch diff --git a/charts/postgres-operator/templates/deployment.yaml b/charts/postgres-operator/templates/deployment.yaml index 1752cb397..ddc3f6a0a 100644 --- a/charts/postgres-operator/templates/deployment.yaml +++ b/charts/postgres-operator/templates/deployment.yaml @@ -52,6 +52,9 @@ spec: {{- if .Values.controllerID.create }} - name: CONTROLLER_ID value: {{ template "postgres-operator.controllerID" . }} + {{- end }} + {{- if .Values.extraEnvs }} + {{- .Values.extraEnvs | toYaml | nindent 12 }} {{- end }} resources: {{ toYaml .Values.resources | indent 10 }} diff --git a/charts/postgres-operator/values.yaml b/charts/postgres-operator/values.yaml index 5700ff783..472be7443 100644 --- a/charts/postgres-operator/values.yaml +++ b/charts/postgres-operator/values.yaml @@ -1,7 +1,7 @@ image: registry: ghcr.io repository: zalando/postgres-operator - tag: v1.12.2 + tag: v1.13.0 pullPolicy: "IfNotPresent" # Optionally specify an array of imagePullSecrets. @@ -38,7 +38,7 @@ configGeneral: # etcd connection string for Patroni. Empty uses K8s-native DCS. etcd_host: "" # Spilo docker image - docker_image: ghcr.io/zalando/spilo-16:3.2-p3 + docker_image: ghcr.io/zalando/spilo-16:3.3-p1 # key name for annotation to ignore globally configured instance limits # ignore_instance_limits_annotation_key: "" @@ -83,7 +83,7 @@ configUsers: configMajorVersionUpgrade: # "off": no upgrade, "manual": manifest triggers action, "full": minimal version violation triggers too - major_version_upgrade_mode: "off" + major_version_upgrade_mode: "manual" # upgrades will only be carried out for clusters of listed teams when mode is "off" # major_version_upgrade_team_allow_list: # - acid @@ -129,8 +129,8 @@ configKubernetes: enable_finalizers: false # enables initContainers to run actions before Spilo is started enable_init_containers: true - # toggles if operator should delete secrets on cluster deletion - enable_secrets_deletion: true + # toggles if child resources should have an owner reference to the postgresql CR + enable_owner_references: false # toggles if operator should delete PVCs on cluster deletion enable_persistent_volume_claim_deletion: true # toggles pod anti affinity on the Postgres pods @@ -139,6 +139,8 @@ configKubernetes: enable_pod_disruption_budget: true # toogles readiness probe for database pods enable_readiness_probe: false + # toggles if operator should delete secrets on cluster deletion + enable_secrets_deletion: true # enables sidecar containers to run alongside Spilo in the same pod enable_sidecars: true @@ -362,7 +364,7 @@ configLogicalBackup: # logical_backup_memory_request: "" # image for pods of the logical backup job (example runs pg_dumpall) - logical_backup_docker_image: "ghcr.io/zalando/postgres-operator/logical-backup:v1.12.2" + logical_backup_docker_image: "ghcr.io/zalando/postgres-operator/logical-backup:v1.13.0" # path of google cloud service account json file # logical_backup_google_application_credentials: "" @@ -478,7 +480,7 @@ priorityClassName: "" # priority class for database pods podPriorityClassName: # If create is false with no name set, no podPriorityClassName is specified. - # Hence, the pod priorityClass is the one with globalDefault set. + # Hence, the pod priorityClass is the one with globalDefault set. # If there is no PriorityClass with globalDefault set, the priority of Pods with no priorityClassName is zero. create: true # If not set a name is generated using the fullname template and "-pod" suffix @@ -504,6 +506,24 @@ readinessProbe: initialDelaySeconds: 5 periodSeconds: 10 +# configure extra environment variables +# Extra environment variables are writen in kubernetes 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 +extraEnvs: + [] + # Exemple of settings maximum amount of memory / cpu that can be used by go process (to match resources.limits) + # - name: MY_VAR + # value: my-value + # - name: GOMAXPROCS + # valueFrom: + # resourceFieldRef: + # resource: limits.cpu + # - name: GOMEMLIMIT + # valueFrom: + # resourceFieldRef: + # resource: limits.memory + # Affinity for pod assignment # Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity affinity: {} diff --git a/delivery.yaml b/delivery.yaml index 03478ac36..7eacd769b 100644 --- a/delivery.yaml +++ b/delivery.yaml @@ -5,27 +5,18 @@ pipeline: vm_config: type: linux size: large + image: cdp-runtime/go cache: paths: - - /go/pkg/mod + - /go/pkg/mod # pkg cache for Go modules + - ~/.cache/go-build # Go build cache commands: - - desc: 'Update' + - desc: Run unit tests cmd: | - apt-get update - - desc: 'Install required build software' + make deps mocks test + + - desc: Build Docker image cmd: | - apt-get install -y make git apt-transport-https ca-certificates curl build-essential python3 python3-pip - - desc: 'Install go' - cmd: | - cd /tmp - wget -q https://storage.googleapis.com/golang/go1.22.3.linux-amd64.tar.gz -O go.tar.gz - tar -xf go.tar.gz - mv go /usr/local - ln -s /usr/local/go/bin/go /usr/bin/go - go version - - desc: 'Build docker image' - cmd: | - export PATH=$PATH:$HOME/go/bin IS_PR_BUILD=${CDP_PULL_REQUEST_NUMBER+"true"} if [[ ${CDP_TARGET_BRANCH} == "master" && ${IS_PR_BUILD} != "true" ]] then @@ -34,23 +25,7 @@ pipeline: IMAGE=registry-write.opensource.zalan.do/acid/postgres-operator-test fi export IMAGE - make deps mocks docker - - desc: 'Run unit tests' - cmd: | - export PATH=$PATH:$HOME/go/bin - go test ./... - - desc: 'Push docker image' - cmd: | - export PATH=$PATH:$HOME/go/bin - IS_PR_BUILD=${CDP_PULL_REQUEST_NUMBER+"true"} - if [[ ${CDP_TARGET_BRANCH} == "master" && ${IS_PR_BUILD} != "true" ]] - then - IMAGE=registry-write.opensource.zalan.do/acid/postgres-operator - else - IMAGE=registry-write.opensource.zalan.do/acid/postgres-operator-test - fi - export IMAGE - make push + make docker push - id: build-operator-ui type: script diff --git a/docker/DebugDockerfile b/docker/DebugDockerfile index 7c7ee8aee..ec1ff6d2f 100644 --- a/docker/DebugDockerfile +++ b/docker/DebugDockerfile @@ -1,18 +1,14 @@ -FROM registry.opensource.zalan.do/library/alpine-3.15:latest +FROM golang:1.22-alpine LABEL maintainer="Team ACID @ Zalando " # We need root certificates to deal with teams api over https -RUN apk --no-cache add ca-certificates go git musl-dev +RUN apk -U add --no-cache ca-certificates delve COPY build/* / RUN addgroup -g 1000 pgo RUN adduser -D -u 1000 -G pgo -g 'Postgres Operator' pgo -RUN go get -d github.com/derekparker/delve/cmd/dlv -RUN cp /root/go/bin/dlv /dlv -RUN chown -R pgo:pgo /dlv - USER pgo:pgo RUN ls -l / diff --git a/docker/Dockerfile b/docker/Dockerfile index d713f1422..b0808c3bc 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,23 +1,20 @@ ARG BASE_IMAGE=registry.opensource.zalan.do/library/alpine-3:latest +FROM golang:1.22-alpine AS builder ARG VERSION=latest -FROM ubuntu:20.04 as builder - -ARG VERSION - COPY . /go/src/github.com/zalando/postgres-operator WORKDIR /go/src/github.com/zalando/postgres-operator -ENV OPERATOR_LDFLAGS="-X=main.version=${VERSION}" -RUN bash docker/build_operator.sh +RUN GO111MODULE=on go mod vendor \ + && CGO_ENABLED=0 go build -o build/postgres-operator -v -ldflags "-X=main.version=${VERSION}" cmd/main.go FROM ${BASE_IMAGE} LABEL maintainer="Team ACID @ Zalando " LABEL org.opencontainers.image.source="https://github.com/zalando/postgres-operator" # We need root certificates to deal with teams api over https -RUN apk --no-cache add curl -RUN apk --no-cache add ca-certificates +RUN apk -U upgrade --no-cache \ + && apk add --no-cache curl ca-certificates COPY --from=builder /go/src/github.com/zalando/postgres-operator/build/* / diff --git a/docker/build_operator.sh b/docker/build_operator.sh index b2dc1dd5d..2ada63a81 100644 --- a/docker/build_operator.sh +++ b/docker/build_operator.sh @@ -13,7 +13,7 @@ apt-get install -y wget ( cd /tmp - wget -q "https://storage.googleapis.com/golang/go1.22.3.linux-${arch}.tar.gz" -O go.tar.gz + wget -q "https://storage.googleapis.com/golang/go1.22.5.linux-${arch}.tar.gz" -O go.tar.gz tar -xf go.tar.gz mv go /usr/local ln -s /usr/local/go/bin/go /usr/bin/go diff --git a/docs/administrator.md b/docs/administrator.md index 7db6fbbc6..a039c503b 100644 --- a/docs/administrator.md +++ b/docs/administrator.md @@ -70,7 +70,7 @@ the manifest. Still, a rolling update would be triggered updating the script will notice the version mismatch and start the old version again. In this scenario the major version could then be run by a user from within the -master pod. Exec into the container and run: +primary pod. Exec into the container and run: ```bash python3 /scripts/inplace_upgrade.py N ``` @@ -81,6 +81,9 @@ upgrade procedure, refer to the [corresponding PR in Spilo](https://github.com/z When `major_version_upgrade_mode` is set to `manual` the operator will run the upgrade script for you after the manifest is updated and pods are rotated. +It is also possible to define `maintenanceWindows` in the Postgres manifest to +better control when such automated upgrades should take place after increasing +the version. ## Non-default cluster domain @@ -223,9 +226,9 @@ configuration: Now, every cluster manifest must contain the configured annotation keys to trigger the delete process when running `kubectl delete pg`. Note, that the -`Postgresql` resource would still get deleted as K8s' API server does not -block it. Only the operator logs will tell, that the delete criteria wasn't -met. +`Postgresql` resource would still get deleted because the operator does not +instruct K8s' API server to block it. Only the operator logs will tell, that +the delete criteria was not met. **cluster manifest** @@ -243,11 +246,64 @@ spec: In case, the resource has been deleted accidentally or the annotations were simply forgotten, it's safe to recreate the cluster with `kubectl create`. -Existing Postgres cluster are not replaced by the operator. But, as the -original cluster still exists the status will show `CreateFailed` at first. -On the next sync event it should change to `Running`. However, as it is in -fact a new resource for K8s, the UID will differ which can trigger a rolling -update of the pods because the UID is used as part of backup path to S3. +Existing Postgres cluster are not replaced by the operator. But, when the +original cluster still exists the status will be `CreateFailed` at first. On +the next sync event it should change to `Running`. However, because it is in +fact a new resource for K8s, the UID and therefore, the backup path to S3, +will differ and trigger a rolling update of the pods. + +## Owner References and Finalizers + +The Postgres Operator can set [owner references](https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/) to most of a cluster's child resources to improve +monitoring with GitOps tools and enable cascading deletes. There are two +exceptions: + +* Persistent Volume Claims, because they are handled by the [PV Reclaim Policy]https://kubernetes.io/docs/tasks/administer-cluster/change-pv-reclaim-policy/ of the Stateful Set +* Cross-namespace secrets, because owner references are not allowed across namespaces by design + +The operator would clean these resources up with its regular delete loop +unless they got synced correctly. If for some reason the initial cluster sync +fails, e.g. after a cluster creation or operator restart, a deletion of the +cluster manifest might leave orphaned resources behind which the user has to +clean up manually. + +Another option is to enable finalizers which first ensures the deletion of all +child resources before the cluster manifest gets removed. There is a trade-off +though: The deletion is only performed after the next two operator SYNC cycles +with the first one setting a `deletionTimestamp` and the latter reacting to it. +The final removal of the custom resource will add a DELETE event to the worker +queue but the child resources are already gone at this point. If you do not +desire this behavior consider enabling owner references instead. + +**postgres-operator ConfigMap** + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: postgres-operator +data: + enable_finalizers: "false" + enable_owner_references: "true" +``` + +**OperatorConfiguration** + +```yaml +apiVersion: "acid.zalan.do/v1" +kind: OperatorConfiguration +metadata: + name: postgresql-operator-configuration +configuration: + kubernetes: + enable_finalizers: false + enable_owner_references: true +``` + +:warning: Please note, both options are disabled by default. When enabling owner +references the operator cannot block cascading deletes, even when the [delete protection annotations](administrator.md#delete-protection-via-annotations) +are in place. You would need an K8s admission controller that blocks the actual +`kubectl delete` API call e.g. based on existing annotations. ## Role-based access control for the operator @@ -1399,7 +1455,7 @@ make docker # build in image in minikube docker env eval $(minikube docker-env) -docker build -t ghcr.io/zalando/postgres-operator-ui:v1.12.2 . +docker build -t ghcr.io/zalando/postgres-operator-ui:v1.13.0 . # apply UI manifests next to a running Postgres Operator kubectl apply -f manifests/ diff --git a/docs/reference/cluster_manifest.md b/docs/reference/cluster_manifest.md index b16d29489..bf731be2e 100644 --- a/docs/reference/cluster_manifest.md +++ b/docs/reference/cluster_manifest.md @@ -114,6 +114,12 @@ These parameters are grouped directly under the `spec` key in the manifest. this parameter. Optional, when empty the load balancer service becomes inaccessible from outside of the Kubernetes cluster. +* **maintenanceWindows** + a list which defines specific time frames when certain maintenance operations + are allowed. So far, it is only implemented for automatic major version + upgrades. Accepted formats are "01:00-06:00" for daily maintenance windows or + "Sat:00:00-04:00" for specific days, with all times in UTC. + * **users** a map of usernames to user flags for the users that should be created in the cluster by the operator. User flags are a list, allowed elements are diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index 1474c5bbe..5b1eb64c9 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -242,7 +242,7 @@ CRD-configuration, they are grouped under the `major_version_upgrade` key. `"manual"` = manifest triggers action, `"full"` = manifest and minimal version violation trigger upgrade. Note, that with all three modes increasing the version in the manifest will - trigger a rolling update of the pods. The default is `"off"`. + trigger a rolling update of the pods. The default is `"manual"`. * **major_version_upgrade_team_allow_list** Upgrades will only be carried out for clusters of listed teams when mode is @@ -263,6 +263,31 @@ Parameters to configure cluster-related Kubernetes objects created by the operator, as well as some timeouts associated with them. In a CRD-based configuration they are grouped under the `kubernetes` key. +* **enable_finalizers** + By default, a deletion of the Postgresql resource will trigger an event + that leads to a cleanup of all child resources. However, if the database + cluster is in a broken state (e.g. failed initialization) and the operator + cannot fully sync it, there can be leftovers. By enabling finalizers the + operator will ensure all managed resources are deleted prior to the + Postgresql resource. See also [admin docs](../administrator.md#owner-references-and-finalizers) + for more information The default is `false`. + +* **enable_owner_references** + The operator can set owner references on its child resources (except PVCs, + Patroni config service/endpoint, cross-namespace secrets) to improve cluster + monitoring and enable cascading deletion. The default is `false`. Warning, + enabling this option disables configured delete protection checks (see below). + +* **delete_annotation_date_key** + key name for annotation that compares manifest value with current date in the + YYYY-MM-DD format. Allowed pattern: `'([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]'`. + The default is empty which also disables this delete protection check. + +* **delete_annotation_name_key** + key name for annotation that compares manifest value with Postgres cluster name. + Allowed pattern: `'([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]'`. The default is + empty which also disables this delete protection check. + * **pod_service_account_name** service account used by Patroni running on individual Pods to communicate with the operator. Required even if native Kubernetes support in Patroni is @@ -293,16 +318,6 @@ configuration they are grouped under the `kubernetes` key. of a database created by the operator. If the annotation key is also provided by the database definition, the database definition value is used. -* **delete_annotation_date_key** - key name for annotation that compares manifest value with current date in the - YYYY-MM-DD format. Allowed pattern: `'([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]'`. - The default is empty which also disables this delete protection check. - -* **delete_annotation_name_key** - key name for annotation that compares manifest value with Postgres cluster name. - Allowed pattern: `'([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]'`. The default is - empty which also disables this delete protection check. - * **downscaler_annotations** An array of annotations that should be passed from Postgres CRD on to the statefulset and, if exists, to the connection pooler deployment as well. @@ -332,20 +347,6 @@ configuration they are grouped under the `kubernetes` key. drained if the node_readiness_label is not used. If this option if set to `false` the `spilo-role=master` selector will not be added to the PDB. -* **enable_finalizers** - By default, a deletion of the Postgresql resource will trigger an event - that leads to a cleanup of all child resources. However, if the database - cluster is in a broken state (e.g. failed initialization) and the operator - cannot fully sync it, there can be leftovers. By enabling finalizers the - operator will ensure all managed resources are deleted prior to the - Postgresql resource. There is a trade-off though: The deletion is only - performed after the next two SYNC cycles with the first one updating the - internal spec and the latter reacting on the `deletionTimestamp` while - processing the SYNC event. The final removal of the custom resource will - add a DELETE event to the worker queue but the child resources are already - gone at this point. - The default is `false`. - * **persistent_volume_claim_retention_policy** The operator tries to protect volumes as much as possible. If somebody accidentally deletes the statefulset or scales in the `numberOfInstances` the @@ -821,7 +822,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 `////logical_backups`. The default image is the same image built with the Zalando-internal CI - pipeline. Default: "ghcr.io/zalando/postgres-operator/logical-backup:v1.12.2" + pipeline. Default: "ghcr.io/zalando/postgres-operator/logical-backup:v1.13.0" * **logical_backup_google_application_credentials** Specifies the path of the google cloud service account json file. Default is empty. diff --git a/docs/user.md b/docs/user.md index 056df7065..78b30dfe9 100644 --- a/docs/user.md +++ b/docs/user.md @@ -758,7 +758,7 @@ If you need to define a `nodeAffinity` for all your Postgres clusters use the ## In-place major version upgrade Starting with Spilo 13, operator supports in-place major version upgrade to a -higher major version (e.g. from PG 11 to PG 13). To trigger the upgrade, +higher major version (e.g. from PG 14 to PG 16). To trigger the upgrade, simply increase the version in the manifest. It is your responsibility to test your applications against the new version before the upgrade; downgrading is not supported. The easiest way to do so is to try the upgrade on the cloned diff --git a/e2e/Makefile b/e2e/Makefile index 9d3681ce5..8e200dab1 100644 --- a/e2e/Makefile +++ b/e2e/Makefile @@ -46,7 +46,7 @@ tools: # install pinned version of 'kind' # go install must run outside of a dir with a (module-based) Go project ! # otherwise go install updates project's dependencies and/or behaves differently - cd "/tmp" && GO111MODULE=on go install sigs.k8s.io/kind@v0.22.0 + cd "/tmp" && GO111MODULE=on go install sigs.k8s.io/kind@v0.23.0 e2etest: tools copy clean ./run.sh main diff --git a/e2e/tests/k8s_api.py b/e2e/tests/k8s_api.py index 12e45f4b0..1f42ad4bc 100644 --- a/e2e/tests/k8s_api.py +++ b/e2e/tests/k8s_api.py @@ -20,6 +20,7 @@ class K8sApi: self.config = config.load_kube_config() self.k8s_client = client.ApiClient() + self.rbac_api = client.RbacAuthorizationV1Api() self.core_v1 = client.CoreV1Api() self.apps_v1 = client.AppsV1Api() @@ -217,7 +218,6 @@ class K8s: 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 if pods: @@ -524,7 +524,6 @@ class K8sBase: 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 if pods: diff --git a/e2e/tests/test_e2e.py b/e2e/tests/test_e2e.py index 10eeca7bf..06e5c5231 100644 --- a/e2e/tests/test_e2e.py +++ b/e2e/tests/test_e2e.py @@ -14,6 +14,7 @@ from kubernetes.client.rest import ApiException SPILO_CURRENT = "registry.opensource.zalan.do/acid/spilo-16-e2e:0.1" SPILO_LAZY = "registry.opensource.zalan.do/acid/spilo-16-e2e:0.2" +SPILO_FULL_IMAGE = "ghcr.io/zalando/spilo-16:3.2-p3" def to_selector(labels): @@ -95,7 +96,7 @@ class EndToEndTestCase(unittest.TestCase): print("Failed to delete the 'standard' storage class: {0}".format(e)) # operator deploys pod service account there on start up - # needed for test_multi_namespace_support() + # needed for test_multi_namespace_support and test_owner_references cls.test_namespace = "test" try: v1_namespace = client.V1Namespace(metadata=client.V1ObjectMeta(name=cls.test_namespace)) @@ -115,6 +116,7 @@ class EndToEndTestCase(unittest.TestCase): configmap = yaml.safe_load(f) configmap["data"]["workers"] = "1" configmap["data"]["docker_image"] = SPILO_CURRENT + configmap["data"]["major_version_upgrade_mode"] = "full" with open("manifests/configmap.yaml", 'w') as f: yaml.dump(configmap, f, Dumper=yaml.Dumper) @@ -129,7 +131,8 @@ class EndToEndTestCase(unittest.TestCase): "infrastructure-roles.yaml", "infrastructure-roles-new.yaml", "custom-team-membership.yaml", - "e2e-storage-class.yaml"]: + "e2e-storage-class.yaml", + "fes.crd.yaml"]: result = k8s.create_with_kubectl("manifests/" + filename) print("stdout: {}, stderr: {}".format(result.stdout, result.stderr)) @@ -199,6 +202,7 @@ class EndToEndTestCase(unittest.TestCase): self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", owner_query)), 3, "Not all additional users found in database", 10, 5) + @timeout_decorator.timeout(TEST_TIMEOUT_SEC) def test_additional_pod_capabilities(self): ''' @@ -398,8 +402,8 @@ class EndToEndTestCase(unittest.TestCase): "max_connections": new_max_connections_value, "wal_level": "logical" } - }, - "patroni": { + }, + "patroni": { "slots": { "first_slot": { "type": "physical" @@ -410,7 +414,7 @@ class EndToEndTestCase(unittest.TestCase): "retry_timeout": 9, "synchronous_mode": True, "failsafe_mode": True, - } + } } } @@ -513,7 +517,7 @@ class EndToEndTestCase(unittest.TestCase): pg_add_new_slots_patch = { "spec": { "patroni": { - "slots": { + "slots": { "test_slot": { "type": "logical", "database": "foo", @@ -909,22 +913,8 @@ class EndToEndTestCase(unittest.TestCase): ''' k8s = self.k8s - annotation_patch = { - "metadata": { - "annotations": { - "k8s-status": "healthy" - }, - } - } try: - sts = k8s.api.apps_v1.read_namespaced_stateful_set('acid-minimal-cluster', 'default') - old_sts_creation_timestamp = sts.metadata.creation_timestamp - k8s.api.apps_v1.patch_namespaced_stateful_set(sts.metadata.name, sts.metadata.namespace, annotation_patch) - svc = k8s.api.core_v1.read_namespaced_service('acid-minimal-cluster', 'default') - old_svc_creation_timestamp = svc.metadata.creation_timestamp - k8s.api.core_v1.patch_namespaced_service(svc.metadata.name, svc.metadata.namespace, annotation_patch) - patch_config_ignored_annotations = { "data": { "ignored_annotations": "k8s-status", @@ -933,6 +923,25 @@ class EndToEndTestCase(unittest.TestCase): k8s.update_config(patch_config_ignored_annotations) self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") + sts = k8s.api.apps_v1.read_namespaced_stateful_set('acid-minimal-cluster', 'default') + svc = k8s.api.core_v1.read_namespaced_service('acid-minimal-cluster', 'default') + + annotation_patch = { + "metadata": { + "annotations": { + "k8s-status": "healthy" + }, + } + } + + old_sts_creation_timestamp = sts.metadata.creation_timestamp + k8s.api.apps_v1.patch_namespaced_stateful_set(sts.metadata.name, sts.metadata.namespace, annotation_patch) + old_svc_creation_timestamp = svc.metadata.creation_timestamp + k8s.api.core_v1.patch_namespaced_service(svc.metadata.name, svc.metadata.namespace, annotation_patch) + + k8s.delete_operator_pod() + self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") + sts = k8s.api.apps_v1.read_namespaced_stateful_set('acid-minimal-cluster', 'default') new_sts_creation_timestamp = sts.metadata.creation_timestamp svc = k8s.api.core_v1.read_namespaced_service('acid-minimal-cluster', 'default') @@ -1174,31 +1183,94 @@ class EndToEndTestCase(unittest.TestCase): 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") + """ + Test major version upgrade + """ + def check_version(): + p = k8s.patroni_rest("acid-upgrade-test-0", "") + version = p.get("server_version", 0) // 10000 + return version - pg_patch_version = { + k8s = self.k8s + cluster_label = 'application=spilo,cluster-name=acid-upgrade-test' + + with open("manifests/minimal-postgres-manifest-12.yaml", 'r+') as f: + upgrade_manifest = yaml.safe_load(f) + upgrade_manifest["spec"]["dockerImage"] = SPILO_FULL_IMAGE + + with open("manifests/minimal-postgres-manifest-12.yaml", 'w') as f: + yaml.dump(upgrade_manifest, f, Dumper=yaml.Dumper) + + k8s.create_with_kubectl("manifests/minimal-postgres-manifest-12.yaml") + self.eventuallyEqual(lambda: k8s.count_running_pods(labels=cluster_label), 2, "No 2 pods running") + self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") + self.eventuallyEqual(check_version, 12, "Version is not correct") + + master_nodes, _ = k8s.get_cluster_nodes(cluster_labels=cluster_label) + # should upgrade immediately + pg_patch_version_14 = { "spec": { - "postgres": { + "postgresql": { "version": "14" } } } k8s.api.custom_objects_api.patch_namespaced_custom_object( - "acid.zalan.do", "v1", "default", "postgresqls", "acid-upgrade-test", pg_patch_version) - + "acid.zalan.do", "v1", "default", "postgresqls", "acid-upgrade-test", pg_patch_version_14) self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") - def check_version_14(): - p = k8s.get_patroni_state("acid-upgrade-test-0") - version = p["server_version"][0:2] - return version + # should have finish failover + k8s.wait_for_pod_failover(master_nodes, 'spilo-role=replica,' + cluster_label) + k8s.wait_for_pod_start('spilo-role=master,' + cluster_label) + k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label) + self.eventuallyEqual(check_version, 14, "Version should be upgraded from 12 to 14") - self.evantuallyEqual(check_version_14, "14", "Version was not upgrade to 14") + # should not upgrade because current time is not in maintenanceWindow + current_time = datetime.now() + maintenance_window_future = f"{(current_time+timedelta(minutes=60)).strftime('%H:%M')}-{(current_time+timedelta(minutes=120)).strftime('%H:%M')}" + pg_patch_version_15 = { + "spec": { + "postgresql": { + "version": "15" + }, + "maintenanceWindows": [ + maintenance_window_future + ] + } + } + k8s.api.custom_objects_api.patch_namespaced_custom_object( + "acid.zalan.do", "v1", "default", "postgresqls", "acid-upgrade-test", pg_patch_version_15) + self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") + + # should have finish failover + k8s.wait_for_pod_failover(master_nodes, 'spilo-role=master,' + cluster_label) + k8s.wait_for_pod_start('spilo-role=master,' + cluster_label) + k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label) + self.eventuallyEqual(check_version, 14, "Version should not be upgraded") + + # change the version again to trigger operator sync + maintenance_window_current = f"{(current_time-timedelta(minutes=30)).strftime('%H:%M')}-{(current_time+timedelta(minutes=30)).strftime('%H:%M')}" + pg_patch_version_16 = { + "spec": { + "postgresql": { + "version": "16" + }, + "maintenanceWindows": [ + maintenance_window_current + ] + } + } + + k8s.api.custom_objects_api.patch_namespaced_custom_object( + "acid.zalan.do", "v1", "default", "postgresqls", "acid-upgrade-test", pg_patch_version_16) + self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") + + # should have finish failover + k8s.wait_for_pod_failover(master_nodes, 'spilo-role=replica,' + cluster_label) + k8s.wait_for_pod_start('spilo-role=master,' + cluster_label) + k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label) + self.eventuallyEqual(check_version, 16, "Version should be upgraded from 14 to 16") @timeout_decorator.timeout(TEST_TIMEOUT_SEC) def test_persistent_volume_claim_retention_policy(self): @@ -1347,17 +1419,11 @@ class EndToEndTestCase(unittest.TestCase): 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") + # acid-test-cluster will be deleted in test_owner_references test except timeout_decorator.TimeoutError: print('Operator log: {}'.format(k8s.get_operator_log())) raise - finally: - # delete the new cluster so that the k8s_api.get_operator_state works correctly in subsequent tests - # ideally we should delete the 'test' namespace here but - # the pods inside the namespace stuck in the Terminating state making the test time out - k8s.api.custom_objects_api.delete_namespaced_custom_object( - "acid.zalan.do", "v1", self.test_namespace, "postgresqls", "acid-test-cluster") - time.sleep(5) @timeout_decorator.timeout(TEST_TIMEOUT_SEC) @unittest.skip("Skipping this test until fixed") @@ -1568,6 +1634,70 @@ class EndToEndTestCase(unittest.TestCase): self.eventuallyEqual(lambda: k8s.count_running_pods("connection-pooler="+pooler_name), 0, "Pooler pods not scaled down") + @timeout_decorator.timeout(TEST_TIMEOUT_SEC) + def test_owner_references(self): + ''' + Enable owner references, test if resources get updated and test cascade deletion of test cluster. + ''' + k8s = self.k8s + cluster_name = 'acid-test-cluster' + cluster_label = 'application=spilo,cluster-name={}'.format(cluster_name) + default_test_cluster = 'acid-minimal-cluster' + + try: + # enable owner references in config + enable_owner_refs = { + "data": { + "enable_owner_references": "true" + } + } + k8s.update_config(enable_owner_refs) + self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") + + time.sleep(5) # wait for the operator to sync the cluster and update resources + + # check if child resources were updated with owner references + self.assertTrue(self.check_cluster_child_resources_owner_references(cluster_name, self.test_namespace), "Owner references not set on all child resources of {}".format(cluster_name)) + self.assertTrue(self.check_cluster_child_resources_owner_references(default_test_cluster), "Owner references not set on all child resources of {}".format(default_test_cluster)) + + # delete the new cluster to test owner references + # and also to make k8s_api.get_operator_state work better in subsequent tests + # ideally we should delete the 'test' namespace here but the pods + # inside the namespace stuck in the Terminating state making the test time out + k8s.api.custom_objects_api.delete_namespaced_custom_object( + "acid.zalan.do", "v1", self.test_namespace, "postgresqls", cluster_name) + + # child resources with owner references should be deleted via owner references + self.eventuallyEqual(lambda: k8s.count_pods_with_label(cluster_label), 0, "Pods not deleted") + self.eventuallyEqual(lambda: k8s.count_statefulsets_with_label(cluster_label), 0, "Statefulset not deleted") + self.eventuallyEqual(lambda: k8s.count_services_with_label(cluster_label), 0, "Services not deleted") + self.eventuallyEqual(lambda: k8s.count_endpoints_with_label(cluster_label), 0, "Endpoints not deleted") + self.eventuallyEqual(lambda: k8s.count_pdbs_with_label(cluster_label), 0, "Pod disruption budget not deleted") + self.eventuallyEqual(lambda: k8s.count_secrets_with_label(cluster_label), 0, "Secrets were not deleted") + + time.sleep(5) # wait for the operator to also delete the PVCs + + # pvcs do not have an owner reference but will deleted by the operator almost immediately + self.eventuallyEqual(lambda: k8s.count_pvcs_with_label(cluster_label), 0, "PVCs not deleted") + + # disable owner references in config + disable_owner_refs = { + "data": { + "enable_owner_references": "false" + } + } + k8s.update_config(disable_owner_refs) + self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") + + time.sleep(5) # wait for the operator to remove owner references + + # check if child resources were updated without Postgresql owner references + self.assertTrue(self.check_cluster_child_resources_owner_references(default_test_cluster, "default", True), "Owner references still present on some child resources of {}".format(default_test_cluster)) + + except timeout_decorator.TimeoutError: + print('Operator log: {}'.format(k8s.get_operator_log())) + raise + @timeout_decorator.timeout(TEST_TIMEOUT_SEC) def test_password_rotation(self): ''' @@ -1766,7 +1896,6 @@ class EndToEndTestCase(unittest.TestCase): 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 @@ -1984,6 +2113,155 @@ class EndToEndTestCase(unittest.TestCase): "acid.zalan.do", "v1", "default", "postgresqls", "acid-standby-cluster") time.sleep(5) + @timeout_decorator.timeout(TEST_TIMEOUT_SEC) + def test_stream_resources(self): + ''' + Create and delete fabric event streaming resources. + ''' + k8s = self.k8s + self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, + "Operator does not get in sync") + leader = k8s.get_cluster_leader_pod() + + # patch ClusterRole with CRUD privileges on FES resources + cluster_role = k8s.api.rbac_api.read_cluster_role("postgres-operator") + fes_cluster_role_rule = client.V1PolicyRule( + api_groups=["zalando.org"], + resources=["fabriceventstreams"], + verbs=["create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"] + ) + cluster_role.rules.append(fes_cluster_role_rule) + + try: + k8s.api.rbac_api.patch_cluster_role("postgres-operator", cluster_role) + + # create a table in one of the database of acid-minimal-cluster + create_stream_table = """ + CREATE TABLE test_table (id int, payload jsonb); + """ + self.query_database(leader.metadata.name, "foo", create_stream_table) + + # update the manifest with the streams section + patch_streaming_config = { + "spec": { + "patroni": { + "slots": { + "manual_slot": { + "type": "physical" + } + } + }, + "streams": [ + { + "applicationId": "test-app", + "batchSize": 100, + "database": "foo", + "enableRecovery": True, + "tables": { + "test_table": { + "eventType": "test-event", + "idColumn": "id", + "payloadColumn": "payload", + "recoveryEventType": "test-event-dlq" + } + } + }, + { + "applicationId": "test-app2", + "batchSize": 100, + "database": "foo", + "enableRecovery": True, + "tables": { + "test_non_exist_table": { + "eventType": "test-event", + "idColumn": "id", + "payloadColumn": "payload", + "recoveryEventType": "test-event-dlq" + } + } + } + ] + } + } + k8s.api.custom_objects_api.patch_namespaced_custom_object( + 'acid.zalan.do', 'v1', 'default', 'postgresqls', 'acid-minimal-cluster', patch_streaming_config) + self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") + + # check if publication, slot, and fes resource are created + get_publication_query = """ + SELECT * FROM pg_publication WHERE pubname = 'fes_foo_test_app'; + """ + get_slot_query = """ + SELECT * FROM pg_replication_slots WHERE slot_name = 'fes_foo_test_app'; + """ + self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "foo", get_publication_query)), 1, + "Publication is not created", 10, 5) + self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "foo", get_slot_query)), 1, + "Replication slot is not created", 10, 5) + self.eventuallyEqual(lambda: len(k8s.api.custom_objects_api.list_namespaced_custom_object( + "zalando.org", "v1", "default", "fabriceventstreams", label_selector="cluster-name=acid-minimal-cluster")["items"]), 1, + "Could not find Fabric Event Stream resource", 10, 5) + + # check if the non-existing table in the stream section does not create a publication and slot + get_publication_query_not_exist_table = """ + SELECT * FROM pg_publication WHERE pubname = 'fes_foo_test_app2'; + """ + get_slot_query_not_exist_table = """ + SELECT * FROM pg_replication_slots WHERE slot_name = 'fes_foo_test_app2'; + """ + self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "foo", get_publication_query_not_exist_table)), 0, + "Publication is created for non-existing tables", 10, 5) + self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "foo", get_slot_query_not_exist_table)), 0, + "Replication slot is created for non-existing tables", 10, 5) + + # grant create and ownership of test_table to foo_user, reset search path to default + grant_permission_foo_user = """ + GRANT CREATE ON DATABASE foo TO foo_user; + ALTER TABLE test_table OWNER TO foo_user; + ALTER ROLE foo_user RESET search_path; + """ + self.query_database(leader.metadata.name, "foo", grant_permission_foo_user) + # non-postgres user creates a publication + create_nonstream_publication = """ + CREATE PUBLICATION mypublication FOR TABLE test_table; + """ + self.query_database_with_user(leader.metadata.name, "foo", create_nonstream_publication, "foo_user") + + # remove the streams section from the manifest + patch_streaming_config_removal = { + "spec": { + "streams": [] + } + } + k8s.api.custom_objects_api.patch_namespaced_custom_object( + 'acid.zalan.do', 'v1', 'default', 'postgresqls', 'acid-minimal-cluster', patch_streaming_config_removal) + self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") + + # check if publication, slot, and fes resource are removed + self.eventuallyEqual(lambda: len(k8s.api.custom_objects_api.list_namespaced_custom_object( + "zalando.org", "v1", "default", "fabriceventstreams", label_selector="cluster-name=acid-minimal-cluster")["items"]), 0, + 'Could not delete Fabric Event Stream resource', 10, 5) + self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "foo", get_publication_query)), 0, + "Publication is not deleted", 10, 5) + self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "foo", get_slot_query)), 0, + "Replication slot is not deleted", 10, 5) + + # check the manual_slot and mypublication should not get deleted + get_manual_slot_query = """ + SELECT * FROM pg_replication_slots WHERE slot_name = 'manual_slot'; + """ + get_nonstream_publication_query = """ + SELECT * FROM pg_publication WHERE pubname = 'mypublication'; + """ + self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", get_manual_slot_query)), 1, + "Slot defined in patroni config is deleted", 10, 5) + self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "foo", get_nonstream_publication_query)), 1, + "Publication defined not in stream section is deleted", 10, 5) + + except timeout_decorator.TimeoutError: + print('Operator log: {}'.format(k8s.get_operator_log())) + raise + @timeout_decorator.timeout(TEST_TIMEOUT_SEC) def test_taint_based_eviction(self): ''' @@ -2110,7 +2388,7 @@ class EndToEndTestCase(unittest.TestCase): self.eventuallyEqual(lambda: k8s.count_statefulsets_with_label(cluster_label), 0, "Statefulset not deleted") self.eventuallyEqual(lambda: k8s.count_deployments_with_label(cluster_label), 0, "Deployments not deleted") self.eventuallyEqual(lambda: k8s.count_pdbs_with_label(cluster_label), 0, "Pod disruption budget not deleted") - self.eventuallyEqual(lambda: k8s.count_secrets_with_label(cluster_label), 7, "Secrets were deleted although disabled in config") + self.eventuallyEqual(lambda: k8s.count_secrets_with_label(cluster_label), 8, "Secrets were deleted although disabled in config") self.eventuallyEqual(lambda: k8s.count_pvcs_with_label(cluster_label), 3, "PVCs were deleted although disabled in config") except timeout_decorator.TimeoutError: @@ -2197,6 +2475,43 @@ class EndToEndTestCase(unittest.TestCase): return True + def check_cluster_child_resources_owner_references(self, cluster_name, cluster_namespace='default', inverse=False): + k8s = self.k8s + + # check if child resources were updated with owner references + sset = k8s.api.apps_v1.read_namespaced_stateful_set(cluster_name, cluster_namespace) + self.assertTrue(self.has_postgresql_owner_reference(sset.metadata.owner_references, inverse), "statefulset owner reference check failed") + + svc = k8s.api.core_v1.read_namespaced_service(cluster_name, cluster_namespace) + self.assertTrue(self.has_postgresql_owner_reference(svc.metadata.owner_references, inverse), "primary service owner reference check failed") + replica_svc = k8s.api.core_v1.read_namespaced_service(cluster_name + "-repl", cluster_namespace) + self.assertTrue(self.has_postgresql_owner_reference(replica_svc.metadata.owner_references, inverse), "replica service owner reference check failed") + config_svc = k8s.api.core_v1.read_namespaced_service(cluster_name + "-config", cluster_namespace) + self.assertTrue(self.has_postgresql_owner_reference(config_svc.metadata.owner_references, inverse), "config service owner reference check failed") + + ep = k8s.api.core_v1.read_namespaced_endpoints(cluster_name, cluster_namespace) + self.assertTrue(self.has_postgresql_owner_reference(ep.metadata.owner_references, inverse), "primary endpoint owner reference check failed") + replica_ep = k8s.api.core_v1.read_namespaced_endpoints(cluster_name + "-repl", cluster_namespace) + self.assertTrue(self.has_postgresql_owner_reference(replica_ep.metadata.owner_references, inverse), "replica endpoint owner reference check failed") + config_ep = k8s.api.core_v1.read_namespaced_endpoints(cluster_name + "-config", cluster_namespace) + self.assertTrue(self.has_postgresql_owner_reference(config_ep.metadata.owner_references, inverse), "config endpoint owner reference check failed") + + pdb = k8s.api.policy_v1.read_namespaced_pod_disruption_budget("postgres-{}-pdb".format(cluster_name), cluster_namespace) + self.assertTrue(self.has_postgresql_owner_reference(pdb.metadata.owner_references, inverse), "pod disruption owner reference check failed") + + pg_secret = k8s.api.core_v1.read_namespaced_secret("postgres.{}.credentials.postgresql.acid.zalan.do".format(cluster_name), cluster_namespace) + self.assertTrue(self.has_postgresql_owner_reference(pg_secret.metadata.owner_references, inverse), "postgres secret owner reference check failed") + standby_secret = k8s.api.core_v1.read_namespaced_secret("standby.{}.credentials.postgresql.acid.zalan.do".format(cluster_name), cluster_namespace) + self.assertTrue(self.has_postgresql_owner_reference(standby_secret.metadata.owner_references, inverse), "standby secret owner reference check failed") + + return True + + def has_postgresql_owner_reference(self, owner_references, inverse): + if inverse: + return owner_references is None or owner_references[0].kind != 'postgresql' + + return owner_references is not None and owner_references[0].kind == 'postgresql' and owner_references[0].controller + def list_databases(self, pod_name): ''' Get list of databases we might want to iterate over diff --git a/go.mod b/go.mod index efa66be36..c1b36d6a5 100644 --- a/go.mod +++ b/go.mod @@ -11,13 +11,13 @@ require ( github.com/r3labs/diff v1.1.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.23.0 + golang.org/x/crypto v0.26.0 golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 gopkg.in/yaml.v2 v2.4.0 - k8s.io/api v0.28.10 + k8s.io/api v0.28.12 k8s.io/apiextensions-apiserver v0.25.9 - k8s.io/apimachinery v0.28.10 - k8s.io/client-go v0.28.10 + k8s.io/apimachinery v0.28.12 + k8s.io/client-go v0.28.12 k8s.io/code-generator v0.25.9 ) @@ -33,7 +33,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/imdario/mergo v0.3.6 // indirect @@ -48,14 +48,15 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.23.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/term v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.17.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 72f94a1ad..f882a95bd 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= @@ -113,31 +113,31 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -145,16 +145,16 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -163,8 +163,8 @@ golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -186,14 +186,14 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.28.10 h1:q1Y+h3F+siuwP/qCQuqgqGJjaIuQWN0yFE7z367E3Q0= -k8s.io/api v0.28.10/go.mod h1:u6EzGdzmEC2vfhyw4sD89i7OIc/2v1EAwvd1t4chQac= +k8s.io/api v0.28.12 h1:C2hpsaso18pqn0Dmkfnbv/YCctozTC3KGGuZ6bF7zhQ= +k8s.io/api v0.28.12/go.mod h1:qjswI+whxvf9LAKD4sEYHfy+WgHGWeH+H5sCRQMwZAQ= k8s.io/apiextensions-apiserver v0.25.9 h1:Pycd6lm2auABp9wKQHCFSEPG+NPdFSTJXPST6NJFzB8= k8s.io/apiextensions-apiserver v0.25.9/go.mod h1:ijGxmSG1GLOEaWhTuaEr0M7KUeia3mWCZa6FFQqpt1M= -k8s.io/apimachinery v0.28.10 h1:cWonrYsJK3lbuf9IgMs5+L5Jzw6QR3ZGA3hzwG0HDeI= -k8s.io/apimachinery v0.28.10/go.mod h1:zUG757HaKs6Dc3iGtKjzIpBfqTM4yiRsEe3/E7NX15o= -k8s.io/client-go v0.28.10 h1:y+mvUei3+RU0rE7r2BZFA2ApTAsXSN1glGs4QfULLt4= -k8s.io/client-go v0.28.10/go.mod h1:JLwjCWhQhvm1F4J+7YAr9WVhSRNmfkRofPWU43m8LZk= +k8s.io/apimachinery v0.28.12 h1:VepMEVOi9o7L/4wMAXJq+3BK9tqBIeerTB+HSOTKeo0= +k8s.io/apimachinery v0.28.12/go.mod h1:zUG757HaKs6Dc3iGtKjzIpBfqTM4yiRsEe3/E7NX15o= +k8s.io/client-go v0.28.12 h1:li7iRPRQF3vDki6gTxT/kXWJvw3BkJSdjVPVhDTZQec= +k8s.io/client-go v0.28.12/go.mod h1:yEzH2Z+nEGlrnKyHJWcJsbOr5tGdIj04dj1TVQOg0wE= k8s.io/code-generator v0.25.9 h1:lgyAV9AIRYNxZxgLRXqsCAtqJLHvakot41CjEqD5W0w= k8s.io/code-generator v0.25.9/go.mod h1:DHfpdhSUrwqF0f4oLqCtF8gYbqlndNetjBEz45nWzJI= k8s.io/gengo v0.0.0-20220902162205-c0856e24416d h1:U9tB195lKdzwqicbJvyJeOXV7Klv+wNAWENRnXEGi08= diff --git a/kubectl-pg/go.mod b/kubectl-pg/go.mod index fb32e08c4..6a658eb49 100644 --- a/kubectl-pg/go.mod +++ b/kubectl-pg/go.mod @@ -3,20 +3,20 @@ module github.com/zalando/postgres-operator/kubectl-pg go 1.22 require ( - github.com/spf13/cobra v1.8.0 - github.com/spf13/viper v1.18.2 - github.com/zalando/postgres-operator v1.12.0 - k8s.io/api v0.28.10 + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 + github.com/zalando/postgres-operator v1.12.2 + k8s.io/api v0.28.12 k8s.io/apiextensions-apiserver v0.25.9 - k8s.io/apimachinery v0.28.10 - k8s.io/client-go v0.28.10 + k8s.io/apimachinery v0.28.12 + k8s.io/client-go v0.28.12 ) require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect @@ -40,7 +40,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect @@ -53,12 +53,12 @@ require ( golang.org/x/crypto v0.23.0 // indirect golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect golang.org/x/net v0.23.0 // indirect - golang.org/x/oauth2 v0.15.0 // indirect + golang.org/x/oauth2 v0.18.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/term v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect golang.org/x/time v0.5.0 // indirect - google.golang.org/appengine v1.6.7 // indirect + google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/kubectl-pg/go.sum b/kubectl-pg/go.sum index d42bafcc5..5d8a2a57f 100644 --- a/kubectl-pg/go.sum +++ b/kubectl-pg/go.sum @@ -1,6 +1,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -13,8 +13,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= @@ -25,11 +25,13 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -80,8 +82,8 @@ github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE= github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -100,15 +102,16 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -121,42 +124,54 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/zalando/postgres-operator v1.12.0 h1:9C5u8UgrVQDRdzB3/T7kKWYKEf2vbF9EZHqtCRSgJtE= -github.com/zalando/postgres-operator v1.12.0/go.mod h1:tKNY4pMjnr5BhuzGiGngf1SPJ7K1vVRCmMkfmV9KZoQ= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zalando/postgres-operator v1.12.2 h1:HJLrGSJLKYkvdpHIxlAKhXWTeRsgDQki2s9QOyApUX0= +github.com/zalando/postgres-operator v1.12.2/go.mod h1:tKNY4pMjnr5BhuzGiGngf1SPJ7K1vVRCmMkfmV9KZoQ= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= -golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= @@ -165,14 +180,17 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -188,14 +206,14 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.28.10 h1:q1Y+h3F+siuwP/qCQuqgqGJjaIuQWN0yFE7z367E3Q0= -k8s.io/api v0.28.10/go.mod h1:u6EzGdzmEC2vfhyw4sD89i7OIc/2v1EAwvd1t4chQac= +k8s.io/api v0.28.12 h1:C2hpsaso18pqn0Dmkfnbv/YCctozTC3KGGuZ6bF7zhQ= +k8s.io/api v0.28.12/go.mod h1:qjswI+whxvf9LAKD4sEYHfy+WgHGWeH+H5sCRQMwZAQ= k8s.io/apiextensions-apiserver v0.25.9 h1:Pycd6lm2auABp9wKQHCFSEPG+NPdFSTJXPST6NJFzB8= k8s.io/apiextensions-apiserver v0.25.9/go.mod h1:ijGxmSG1GLOEaWhTuaEr0M7KUeia3mWCZa6FFQqpt1M= -k8s.io/apimachinery v0.28.10 h1:cWonrYsJK3lbuf9IgMs5+L5Jzw6QR3ZGA3hzwG0HDeI= -k8s.io/apimachinery v0.28.10/go.mod h1:zUG757HaKs6Dc3iGtKjzIpBfqTM4yiRsEe3/E7NX15o= -k8s.io/client-go v0.28.10 h1:y+mvUei3+RU0rE7r2BZFA2ApTAsXSN1glGs4QfULLt4= -k8s.io/client-go v0.28.10/go.mod h1:JLwjCWhQhvm1F4J+7YAr9WVhSRNmfkRofPWU43m8LZk= +k8s.io/apimachinery v0.28.12 h1:VepMEVOi9o7L/4wMAXJq+3BK9tqBIeerTB+HSOTKeo0= +k8s.io/apimachinery v0.28.12/go.mod h1:zUG757HaKs6Dc3iGtKjzIpBfqTM4yiRsEe3/E7NX15o= +k8s.io/client-go v0.28.12 h1:li7iRPRQF3vDki6gTxT/kXWJvw3BkJSdjVPVhDTZQec= +k8s.io/client-go v0.28.12/go.mod h1:yEzH2Z+nEGlrnKyHJWcJsbOr5tGdIj04dj1TVQOg0wE= k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= diff --git a/manifests/complete-postgres-manifest.yaml b/manifests/complete-postgres-manifest.yaml index f874123e6..0b3dc4aa7 100644 --- a/manifests/complete-postgres-manifest.yaml +++ b/manifests/complete-postgres-manifest.yaml @@ -10,7 +10,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: ghcr.io/zalando/spilo-16:3.2-p3 + dockerImage: ghcr.io/zalando/spilo-16:3.3-p1 teamId: "acid" numberOfInstances: 2 users: # Application/Robot users diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml index 7f76d0b33..1c8c8fdfd 100644 --- a/manifests/configmap.yaml +++ b/manifests/configmap.yaml @@ -18,11 +18,11 @@ data: connection_pooler_default_memory_limit: 100Mi connection_pooler_default_memory_request: 100Mi connection_pooler_image: "registry.opensource.zalan.do/acid/pgbouncer:master-32" - # connection_pooler_max_db_connections: 60 - # connection_pooler_mode: "transaction" - # connection_pooler_number_of_instances: 2 - # connection_pooler_schema: "pooler" - # connection_pooler_user: "pooler" + connection_pooler_max_db_connections: "60" + connection_pooler_mode: "transaction" + connection_pooler_number_of_instances: "2" + connection_pooler_schema: "pooler" + connection_pooler_user: "pooler" crd_categories: "all" # custom_service_annotations: "keyx:valuez,keya:valuea" # custom_pod_annotations: "keya:valuea,keyb:valueb" @@ -34,39 +34,41 @@ data: default_memory_request: 100Mi # delete_annotation_date_key: delete-date # delete_annotation_name_key: delete-clustername - docker_image: ghcr.io/zalando/spilo-16:3.2-p3 + docker_image: ghcr.io/zalando/spilo-16:3.3-p1 # downscaler_annotations: "deployment-time,downscaler/*" - # enable_admin_role_for_users: "true" - # enable_crd_registration: "true" - # enable_cross_namespace_secret: "false" + enable_admin_role_for_users: "true" + enable_crd_registration: "true" + enable_crd_validation: "true" + enable_cross_namespace_secret: "false" enable_finalizers: "false" - # enable_database_access: "true" + enable_database_access: "true" enable_ebs_gp3_migration: "false" - # enable_ebs_gp3_migration_max_size: "1000" - # enable_init_containers: "true" - # enable_lazy_spilo_upgrade: "false" + enable_ebs_gp3_migration_max_size: "1000" + enable_init_containers: "true" + enable_lazy_spilo_upgrade: "false" enable_master_load_balancer: "false" enable_master_pooler_load_balancer: "false" enable_password_rotation: "false" enable_patroni_failsafe_mode: "false" - enable_secrets_deletion: "true" + enable_owner_references: "false" enable_persistent_volume_claim_deletion: "true" enable_pgversion_env_var: "true" - # enable_pod_antiaffinity: "false" - # enable_pod_disruption_budget: "true" - # enable_postgres_team_crd: "false" - # enable_postgres_team_crd_superusers: "false" + enable_pod_antiaffinity: "false" + enable_pod_disruption_budget: "true" + enable_postgres_team_crd: "false" + enable_postgres_team_crd_superusers: "false" enable_readiness_probe: "false" enable_replica_load_balancer: "false" enable_replica_pooler_load_balancer: "false" - # enable_shm_volume: "true" - # enable_sidecars: "true" + enable_secrets_deletion: "true" + enable_shm_volume: "true" + enable_sidecars: "true" enable_spilo_wal_path_compat: "true" enable_team_id_clustername_prefix: "false" enable_team_member_deprecation: "false" - # enable_team_superuser: "false" + enable_team_superuser: "false" enable_teams_api: "false" - # etcd_host: "" + etcd_host: "" external_traffic_policy: "Cluster" # gcp_credentials: "" # ignored_annotations: "" @@ -76,56 +78,55 @@ data: # inherited_annotations: owned-by # inherited_labels: application,environment # kube_iam_role: "" - # kubernetes_use_configmaps: "false" + kubernetes_use_configmaps: "false" # log_s3_bucket: "" # logical_backup_azure_storage_account_name: "" # logical_backup_azure_storage_container: "" # logical_backup_azure_storage_account_key: "" # logical_backup_cpu_limit: "" # logical_backup_cpu_request: "" - logical_backup_docker_image: "ghcr.io/zalando/postgres-operator/logical-backup:v1.12.2" + logical_backup_cronjob_environment_secret: "" + logical_backup_docker_image: "ghcr.io/zalando/postgres-operator/logical-backup:v1.13.0" # logical_backup_google_application_credentials: "" logical_backup_job_prefix: "logical-backup-" # logical_backup_memory_limit: "" # logical_backup_memory_request: "" logical_backup_provider: "s3" - # logical_backup_s3_access_key_id: "" + logical_backup_s3_access_key_id: "" logical_backup_s3_bucket: "my-bucket-url" - # logical_backup_s3_bucket_prefix: "spilo" - # logical_backup_s3_region: "" - # logical_backup_s3_endpoint: "" - # logical_backup_s3_secret_access_key: "" + logical_backup_s3_bucket_prefix: "spilo" + logical_backup_s3_region: "" + logical_backup_s3_endpoint: "" + logical_backup_s3_secret_access_key: "" logical_backup_s3_sse: "AES256" - # logical_backup_s3_retention_time: "" + logical_backup_s3_retention_time: "" logical_backup_schedule: "30 00 * * *" - # logical_backup_cronjob_environment_secret: "" major_version_upgrade_mode: "manual" # major_version_upgrade_team_allow_list: "" master_dns_name_format: "{cluster}.{namespace}.{hostedzone}" - # master_legacy_dns_name_format: "{cluster}.{team}.{hostedzone}" - # master_pod_move_timeout: 20m - # max_instances: "-1" - # min_instances: "-1" + master_legacy_dns_name_format: "{cluster}.{team}.{hostedzone}" + master_pod_move_timeout: 20m # max_cpu_request: "1" + max_instances: "-1" # max_memory_request: 4Gi - # min_cpu_limit: 250m - # min_memory_limit: 250Mi - # minimal_major_version: "12" + min_cpu_limit: 250m + min_instances: "-1" + min_memory_limit: 250Mi + minimal_major_version: "12" # node_readiness_label: "status:ready" # node_readiness_label_merge: "OR" - # oauth_token_secret_name: postgresql-operator - # pam_configuration: | - # https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees - # pam_role_name: zalandos + oauth_token_secret_name: postgresql-operator + pam_configuration: "https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees" + pam_role_name: zalandos patroni_api_check_interval: "1s" patroni_api_check_timeout: "5s" - # password_rotation_interval: "90" - # password_rotation_user_retention: "180" + password_rotation_interval: "90" + password_rotation_user_retention: "180" pdb_master_label_selector: "true" pdb_name_format: "postgres-{cluster}-pdb" persistent_volume_claim_retention_policy: "when_deleted:retain,when_scaled:retain" - # pod_antiaffinity_preferred_during_scheduling: "false" - # pod_antiaffinity_topology_key: "kubernetes.io/hostname" + pod_antiaffinity_preferred_during_scheduling: "false" + pod_antiaffinity_topology_key: "kubernetes.io/hostname" pod_deletion_wait_timeout: 10m # pod_environment_configmap: "default/my-custom-config" # pod_environment_secret: "my-custom-secret" @@ -133,17 +134,17 @@ data: pod_management_policy: "ordered_ready" # pod_priority_class_name: "postgres-pod-priority" pod_role_label: spilo-role - # pod_service_account_definition: "" + pod_service_account_definition: "" pod_service_account_name: "postgres-pod" - # pod_service_account_role_binding_definition: "" + pod_service_account_role_binding_definition: "" pod_terminate_grace_period: 5m - # postgres_superuser_teams: "postgres_superusers" - # protected_role_names: "admin,cron_admin" + postgres_superuser_teams: "postgres_superusers" + protected_role_names: "admin,cron_admin" ready_wait_interval: 3s ready_wait_timeout: 30s repair_period: 5m replica_dns_name_format: "{cluster}-repl.{namespace}.{hostedzone}" - # replica_legacy_dns_name_format: "{cluster}-repl.{team}.{hostedzone}" + replica_legacy_dns_name_format: "{cluster}-repl.{team}.{hostedzone}" replication_username: standby resource_check_interval: 3s resource_check_timeout: 10m @@ -153,7 +154,7 @@ data: secret_name_template: "{username}.{cluster}.credentials.{tprkind}.{tprgroup}" share_pgsocket_with_sidecars: "false" # sidecar_docker_images: "" - # set_memory_request_to_limit: "false" + set_memory_request_to_limit: "false" spilo_allow_privilege_escalation: "true" # spilo_runasuser: 101 # spilo_runasgroup: 103 @@ -161,10 +162,10 @@ data: spilo_privileged: "false" storage_resize_mode: "pvc" super_username: postgres - # target_major_version: "16" - # team_admin_role: "admin" - # team_api_role_configuration: "log_statement:all" - # teams_api_url: http://fake-teams-api.default.svc.cluster.local + target_major_version: "16" + team_admin_role: "admin" + team_api_role_configuration: "log_statement:all" + teams_api_url: http://fake-teams-api.default.svc.cluster.local # toleration: "key:db-only,operator:Exists,effect:NoSchedule" # wal_az_storage_account: "" # wal_gs_bucket: "" diff --git a/manifests/fes.crd.yaml b/manifests/fes.crd.yaml new file mode 100644 index 000000000..70a8c9555 --- /dev/null +++ b/manifests/fes.crd.yaml @@ -0,0 +1,23 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: fabriceventstreams.zalando.org +spec: + group: zalando.org + names: + kind: FabricEventStream + listKind: FabricEventStreamList + plural: fabriceventstreams + singular: fabriceventstream + shortNames: + - fes + categories: + - all + scope: Namespaced + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object diff --git a/manifests/operator-service-account-rbac-openshift.yaml b/manifests/operator-service-account-rbac-openshift.yaml index e0e45cc54..e716e82b7 100644 --- a/manifests/operator-service-account-rbac-openshift.yaml +++ b/manifests/operator-service-account-rbac-openshift.yaml @@ -94,6 +94,7 @@ rules: - create - delete - get + - patch - update # to check nodes for node readiness label - apiGroups: @@ -166,6 +167,7 @@ rules: - get - list - patch + - update # to CRUD cron jobs for logical backups - apiGroups: - batch diff --git a/manifests/operator-service-account-rbac.yaml b/manifests/operator-service-account-rbac.yaml index c10dc5fd7..bf27f99f1 100644 --- a/manifests/operator-service-account-rbac.yaml +++ b/manifests/operator-service-account-rbac.yaml @@ -102,6 +102,7 @@ rules: - delete - get - update + - patch # to check nodes for node readiness label - apiGroups: - "" @@ -173,6 +174,7 @@ rules: - get - list - patch + - update # to CRUD cron jobs for logical backups - apiGroups: - batch diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 887577940..c2b0cf398 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -66,7 +66,7 @@ spec: type: string docker_image: type: string - default: "ghcr.io/zalando/spilo-16:3.2-p3" + default: "ghcr.io/zalando/spilo-16:3.3-p1" enable_crd_registration: type: boolean default: true @@ -158,7 +158,7 @@ spec: properties: major_version_upgrade_mode: type: string - default: "off" + default: "manual" major_version_upgrade_team_allow_list: type: array items: @@ -209,9 +209,9 @@ spec: enable_init_containers: type: boolean default: true - enable_secrets_deletion: + enable_owner_references: type: boolean - default: true + default: false enable_persistent_volume_claim_deletion: type: boolean default: true @@ -224,6 +224,9 @@ spec: enable_readiness_probe: type: boolean default: false + enable_secrets_deletion: + type: boolean + default: true enable_sidecars: type: boolean default: true @@ -467,7 +470,6 @@ spec: type: string additional_secret_mount_path: type: string - default: "/meta/credentials" aws_region: type: string default: "eu-central-1" @@ -506,7 +508,7 @@ spec: pattern: '^(\d+m|\d+(\.\d{1,3})?)$' logical_backup_docker_image: type: string - default: "ghcr.io/zalando/postgres-operator/logical-backup:v1.12.2" + default: "ghcr.io/zalando/postgres-operator/logical-backup:v1.13.0" logical_backup_google_application_credentials: type: string logical_backup_job_prefix: diff --git a/manifests/postgres-operator.yaml b/manifests/postgres-operator.yaml index d0890bd34..fbba84c7f 100644 --- a/manifests/postgres-operator.yaml +++ b/manifests/postgres-operator.yaml @@ -19,7 +19,7 @@ spec: serviceAccountName: postgres-operator containers: - name: postgres-operator - image: ghcr.io/zalando/postgres-operator:v1.12.2 + image: ghcr.io/zalando/postgres-operator:v1.13.0 imagePullPolicy: IfNotPresent resources: requests: diff --git a/manifests/postgresql-operator-default-configuration.yaml b/manifests/postgresql-operator-default-configuration.yaml index ee3123e32..ecb7a03de 100644 --- a/manifests/postgresql-operator-default-configuration.yaml +++ b/manifests/postgresql-operator-default-configuration.yaml @@ -3,7 +3,7 @@ kind: OperatorConfiguration metadata: name: postgresql-operator-default-configuration configuration: - docker_image: ghcr.io/zalando/spilo-16:3.2-p3 + docker_image: ghcr.io/zalando/spilo-16:3.3-p1 # enable_crd_registration: true # crd_categories: # - all @@ -36,7 +36,7 @@ configuration: replication_username: standby super_username: postgres major_version_upgrade: - major_version_upgrade_mode: "off" + major_version_upgrade_mode: "manual" # major_version_upgrade_team_allow_list: # - acid minimal_major_version: "12" @@ -59,11 +59,12 @@ configuration: # enable_cross_namespace_secret: "false" enable_finalizers: false enable_init_containers: true - enable_secrets_deletion: true + enable_owner_references: false enable_persistent_volume_claim_deletion: true enable_pod_antiaffinity: false enable_pod_disruption_budget: true enable_readiness_probe: false + enable_secrets_deletion: true enable_sidecars: true # ignored_annotations: # - k8s.v1.cni.cncf.io/network-status @@ -167,7 +168,7 @@ configuration: # logical_backup_cpu_request: "" # logical_backup_memory_limit: "" # logical_backup_memory_request: "" - logical_backup_docker_image: "ghcr.io/zalando/postgres-operator/logical-backup:v1.12.2" + logical_backup_docker_image: "ghcr.io/zalando/postgres-operator/logical-backup:v1.13.0" # logical_backup_google_application_credentials: "" logical_backup_job_prefix: "logical-backup-" logical_backup_provider: "s3" diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index 4bd757f38..9207c83d4 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -224,7 +224,7 @@ spec: type: array 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))\ *$' + pattern: '^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ *$' masterServiceAnnotations: type: object additionalProperties: @@ -373,7 +373,6 @@ spec: version: type: string enum: - - "11" - "12" - "13" - "14" diff --git a/pkg/apis/acid.zalan.do/v1/crds.go b/pkg/apis/acid.zalan.do/v1/crds.go index 9e65869e7..da88b0855 100644 --- a/pkg/apis/acid.zalan.do/v1/crds.go +++ b/pkg/apis/acid.zalan.do/v1/crds.go @@ -595,9 +595,6 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{ "version": { Type: "string", Enum: []apiextv1.JSON{ - { - Raw: []byte(`"11"`), - }, { Raw: []byte(`"12"`), }, @@ -1329,7 +1326,7 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{ "enable_init_containers": { Type: "boolean", }, - "enable_secrets_deletion": { + "enable_owner_references": { Type: "boolean", }, "enable_persistent_volume_claim_deletion": { @@ -1344,6 +1341,9 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{ "enable_readiness_probe": { Type: "boolean", }, + "enable_secrets_deletion": { + Type: "boolean", + }, "enable_sidecars": { Type: "boolean", }, diff --git a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go index 48fd0a13c..eb01d450c 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -47,7 +47,7 @@ type PostgresUsersConfiguration struct { // MajorVersionUpgradeConfiguration defines how to execute major version upgrades of Postgres. type MajorVersionUpgradeConfiguration struct { - MajorVersionUpgradeMode string `json:"major_version_upgrade_mode" default:"off"` // off - no actions, manual - manifest triggers action, full - manifest and minimal version violation trigger upgrade + MajorVersionUpgradeMode string `json:"major_version_upgrade_mode" default:"manual"` // off - no actions, manual - manifest triggers action, full - manifest and minimal version violation trigger upgrade MajorVersionUpgradeTeamAllowList []string `json:"major_version_upgrade_team_allow_list,omitempty"` MinimalMajorVersion string `json:"minimal_major_version" default:"12"` TargetMajorVersion string `json:"target_major_version" default:"16"` @@ -55,6 +55,7 @@ type MajorVersionUpgradeConfiguration struct { // KubernetesMetaConfiguration defines k8s conf required for all Postgres clusters and the operator itself type KubernetesMetaConfiguration struct { + EnableOwnerReferences *bool `json:"enable_owner_references,omitempty"` PodServiceAccountName string `json:"pod_service_account_name,omitempty"` // TODO: change it to the proper json PodServiceAccountDefinition string `json:"pod_service_account_definition,omitempty"` @@ -159,7 +160,7 @@ type AWSGCPConfiguration struct { LogS3Bucket string `json:"log_s3_bucket,omitempty"` KubeIAMRole string `json:"kube_iam_role,omitempty"` AdditionalSecretMount string `json:"additional_secret_mount,omitempty"` - AdditionalSecretMountPath string `json:"additional_secret_mount_path" default:"/meta/credentials"` + AdditionalSecretMountPath string `json:"additional_secret_mount_path,omitempty"` EnableEBSGp3Migration bool `json:"enable_ebs_gp3_migration" default:"false"` EnableEBSGp3MigrationMaxSize int64 `json:"enable_ebs_gp3_migration_max_size" default:"1000"` } diff --git a/pkg/apis/acid.zalan.do/v1/postgresql_type.go b/pkg/apis/acid.zalan.do/v1/postgresql_type.go index 612cf7041..3d731743f 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql_type.go +++ b/pkg/apis/acid.zalan.do/v1/postgresql_type.go @@ -133,7 +133,7 @@ type Volume struct { Size string `json:"size"` StorageClass string `json:"storageClass,omitempty"` SubPath string `json:"subPath,omitempty"` - IsSubPathExpr *bool `json:"isSubPathExpr,omitemtpy"` + IsSubPathExpr *bool `json:"isSubPathExpr,omitempty"` Iops *int64 `json:"iops,omitempty"` Throughput *int64 `json:"throughput,omitempty"` VolumeType string `json:"type,omitempty"` @@ -144,7 +144,7 @@ type AdditionalVolume struct { Name string `json:"name"` MountPath string `json:"mountPath"` SubPath string `json:"subPath,omitempty"` - IsSubPathExpr *bool `json:"isSubPathExpr,omitemtpy"` + IsSubPathExpr *bool `json:"isSubPathExpr,omitempty"` TargetContainers []string `json:"targetContainers"` VolumeSource v1.VolumeSource `json:"volumeSource"` } diff --git a/pkg/apis/acid.zalan.do/v1/util_test.go b/pkg/apis/acid.zalan.do/v1/util_test.go index bb01816c0..bef6cc3ec 100644 --- a/pkg/apis/acid.zalan.do/v1/util_test.go +++ b/pkg/apis/acid.zalan.do/v1/util_test.go @@ -123,6 +123,8 @@ var maintenanceWindows = []struct { {"expect error as weekday is empty", []byte(`":00:00-10:00"`), MaintenanceWindow{}, errors.New(`could not parse weekday: incorrect weekday`)}, {"expect error as maintenance window set seconds", []byte(`"Mon:00:00:00-10:00:00"`), MaintenanceWindow{}, errors.New(`incorrect maintenance window format`)}, {"expect error as 'To' time set seconds", []byte(`"Mon:00:00-00:00:00"`), MaintenanceWindow{}, errors.New("could not parse end time: incorrect time format")}, + // ideally, should be implemented + {"expect error as 'To' has a weekday", []byte(`"Mon:00:00-Fri:00:00"`), MaintenanceWindow{}, errors.New("could not parse end time: incorrect time format")}, {"expect error as 'To' time is missing", []byte(`"Mon:00:00"`), MaintenanceWindow{}, errors.New("incorrect maintenance window format")}} var postgresStatus = []struct { diff --git a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go index 80bc7b34d..557f8889c 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -158,6 +158,11 @@ func (in *ConnectionPoolerConfiguration) DeepCopy() *ConnectionPoolerConfigurati // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KubernetesMetaConfiguration) DeepCopyInto(out *KubernetesMetaConfiguration) { *out = *in + if in.EnableOwnerReferences != nil { + in, out := &in.EnableOwnerReferences, &out.EnableOwnerReferences + *out = new(bool) + **out = **in + } if in.SpiloAllowPrivilegeEscalation != nil { in, out := &in.SpiloAllowPrivilegeEscalation, &out.SpiloAllowPrivilegeEscalation *out = new(bool) diff --git a/pkg/apis/zalando.org/v1/fabriceventstream.go b/pkg/apis/zalando.org/v1/fabriceventstream.go index 609f3c9bc..41bb5e80c 100644 --- a/pkg/apis/zalando.org/v1/fabriceventstream.go +++ b/pkg/apis/zalando.org/v1/fabriceventstream.go @@ -1,6 +1,7 @@ package v1 import ( + acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -89,3 +90,8 @@ type DBAuth struct { UserKey string `json:"userKey,omitempty"` PasswordKey string `json:"passwordKey,omitempty"` } + +type Slot struct { + Slot map[string]string `json:"slot"` + Publication map[string]acidv1.StreamTable `json:"publication"` +} diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index fe8f3195b..d9997463a 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -3,7 +3,6 @@ package cluster // Postgres CustomResourceDefinition object i.e. Spilo import ( - "context" "database/sql" "encoding/json" "fmt" @@ -15,6 +14,7 @@ import ( "github.com/sirupsen/logrus" acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" + zalandov1 "github.com/zalando/postgres-operator/pkg/apis/zalando.org/v1" "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/scheme" "github.com/zalando/postgres-operator/pkg/spec" @@ -61,9 +61,13 @@ type Config struct { type kubeResources struct { Services map[PostgresRole]*v1.Service Endpoints map[PostgresRole]*v1.Endpoints + PatroniEndpoints map[string]*v1.Endpoints + PatroniConfigMaps map[string]*v1.ConfigMap Secrets map[types.UID]*v1.Secret Statefulset *appsv1.StatefulSet PodDisruptionBudget *policyv1.PodDisruptionBudget + LogicalBackupJob *batchv1.CronJob + Streams map[string]*zalandov1.FabricEventStream //Pods are treated separately //PVCs are treated separately } @@ -131,9 +135,12 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres systemUsers: make(map[string]spec.PgUser), podSubscribers: make(map[spec.NamespacedName]chan PodEvent), kubeResources: kubeResources{ - Secrets: make(map[types.UID]*v1.Secret), - Services: make(map[PostgresRole]*v1.Service), - Endpoints: make(map[PostgresRole]*v1.Endpoints)}, + Secrets: make(map[types.UID]*v1.Secret), + Services: make(map[PostgresRole]*v1.Service), + Endpoints: make(map[PostgresRole]*v1.Endpoints), + PatroniEndpoints: make(map[string]*v1.Endpoints), + PatroniConfigMaps: make(map[string]*v1.ConfigMap), + Streams: make(map[string]*zalandov1.FabricEventStream)}, userSyncStrategy: users.DefaultUserSyncStrategy{ PasswordEncryption: passwordEncryption, RoleDeletionSuffix: cfg.OpConfig.RoleDeletionSuffix, @@ -356,6 +363,11 @@ func (c *Cluster) Create() (err error) { c.logger.Infof("pods are ready") c.eventRecorder.Event(c.GetReference(), v1.EventTypeNormal, "StatefulSet", "Pods are ready") + // sync resources created by Patroni + if err = c.syncPatroniResources(); err != nil { + c.logger.Warnf("Patroni resources not yet synced: %v", err) + } + // create database objects unless we are running without pods or disabled // that feature explicitly if !(c.databaseAccessDisabled() || c.getNumberOfInstances(&c.Spec) <= 0 || c.Spec.StandbyCluster != nil) { @@ -381,10 +393,6 @@ func (c *Cluster) Create() (err error) { c.logger.Info("a k8s cron job for logical backup has been successfully created") } - if err := c.listResources(); err != nil { - c.logger.Errorf("could not list resources: %v", err) - } - // Create connection pooler deployment and services if necessary. Since we // need to perform some operations with the database itself (e.g. install // lookup function), do it as the last step, when everything is available. @@ -409,6 +417,10 @@ func (c *Cluster) Create() (err error) { } } + if err := c.listResources(); err != nil { + c.logger.Errorf("could not list resources: %v", err) + } + return nil } @@ -422,6 +434,11 @@ func (c *Cluster) compareStatefulSetWith(statefulSet *appsv1.StatefulSet) *compa match = false reasons = append(reasons, "new statefulset's number of replicas does not match the current one") } + if !reflect.DeepEqual(c.Statefulset.OwnerReferences, statefulSet.OwnerReferences) { + match = false + needsReplace = true + reasons = append(reasons, "new statefulset's ownerReferences do not match") + } if changed, reason := c.compareAnnotations(c.Statefulset.Annotations, statefulSet.Annotations); changed { match = false needsReplace = true @@ -433,6 +450,12 @@ func (c *Cluster) compareStatefulSetWith(statefulSet *appsv1.StatefulSet) *compa reasons = append(reasons, "new statefulset's pod management policy do not match") } + if c.Statefulset.Spec.PersistentVolumeClaimRetentionPolicy == nil { + c.Statefulset.Spec.PersistentVolumeClaimRetentionPolicy = &appsv1.StatefulSetPersistentVolumeClaimRetentionPolicy{ + WhenDeleted: appsv1.RetainPersistentVolumeClaimRetentionPolicyType, + WhenScaled: appsv1.RetainPersistentVolumeClaimRetentionPolicyType, + } + } if !reflect.DeepEqual(c.Statefulset.Spec.PersistentVolumeClaimRetentionPolicy, statefulSet.Spec.PersistentVolumeClaimRetentionPolicy) { match = false needsReplace = true @@ -493,7 +516,6 @@ func (c *Cluster) compareStatefulSetWith(statefulSet *appsv1.StatefulSet) *compa if changed, reason := c.compareAnnotations(c.Statefulset.Spec.Template.Annotations, statefulSet.Spec.Template.Annotations); changed { match = false needsReplace = true - needsRollUpdate = true reasons = append(reasons, "new statefulset's pod template metadata annotations does not match "+reason) } if !reflect.DeepEqual(c.Statefulset.Spec.Template.Spec.SecurityContext, statefulSet.Spec.Template.Spec.SecurityContext) { @@ -513,9 +535,9 @@ func (c *Cluster) compareStatefulSetWith(statefulSet *appsv1.StatefulSet) *compa reasons = append(reasons, fmt.Sprintf("new statefulset's name for volume %d does not match the current one", i)) continue } - if !reflect.DeepEqual(c.Statefulset.Spec.VolumeClaimTemplates[i].Annotations, statefulSet.Spec.VolumeClaimTemplates[i].Annotations) { + if changed, reason := c.compareAnnotations(c.Statefulset.Spec.VolumeClaimTemplates[i].Annotations, statefulSet.Spec.VolumeClaimTemplates[i].Annotations); changed { needsReplace = true - reasons = append(reasons, fmt.Sprintf("new statefulset's annotations for volume %q does not match the current one", name)) + reasons = append(reasons, fmt.Sprintf("new statefulset's annotations for volume %q do not match the current ones: %s", name, reason)) } if !reflect.DeepEqual(c.Statefulset.Spec.VolumeClaimTemplates[i].Spec, statefulSet.Spec.VolumeClaimTemplates[i].Spec) { name := c.Statefulset.Spec.VolumeClaimTemplates[i].Name @@ -591,7 +613,7 @@ func (c *Cluster) compareContainers(description string, setA, setB []v1.Containe newCheck("new %s's %s (index %d) security context does not match the current one", func(a, b v1.Container) bool { return !reflect.DeepEqual(a.SecurityContext, b.SecurityContext) }), newCheck("new %s's %s (index %d) volume mounts do not match the current one", - func(a, b v1.Container) bool { return !reflect.DeepEqual(a.VolumeMounts, b.VolumeMounts) }), + func(a, b v1.Container) bool { return !compareVolumeMounts(a.VolumeMounts, b.VolumeMounts) }), } if !c.OpConfig.EnableLazySpiloUpgrade { @@ -646,7 +668,7 @@ func compareEnv(a, b []v1.EnvVar) bool { if len(a) != len(b) { return false } - equal := true + var equal bool for _, enva := range a { hasmatch := false for _, envb := range b { @@ -732,6 +754,27 @@ func comparePorts(a, b []v1.ContainerPort) bool { return true } +func compareVolumeMounts(old, new []v1.VolumeMount) bool { + if len(old) != len(new) { + return false + } + for _, mount := range old { + if !volumeMountExists(mount, new) { + return false + } + } + return true +} + +func volumeMountExists(mount v1.VolumeMount, mounts []v1.VolumeMount) bool { + for _, m := range mounts { + if reflect.DeepEqual(mount, m) { + return true + } + } + return false +} + func (c *Cluster) compareAnnotations(old, new map[string]string) (bool, string) { reason := "" ignoredAnnotations := make(map[string]bool) @@ -780,8 +823,8 @@ func (c *Cluster) compareServices(old, new *v1.Service) (bool, string) { } } - if changed, reason := c.compareAnnotations(old.Annotations, new.Annotations); changed { - return !changed, "new service's annotations does not match the current one:" + reason + if !reflect.DeepEqual(old.ObjectMeta.OwnerReferences, new.ObjectMeta.OwnerReferences) { + return false, "new service's owner references do not match the current ones" } return true, "" @@ -801,6 +844,12 @@ func (c *Cluster) compareLogicalBackupJob(cur, new *batchv1.CronJob) (match bool newImage, curImage) } + newPodAnnotation := new.Spec.JobTemplate.Spec.Template.Annotations + curPodAnnotation := cur.Spec.JobTemplate.Spec.Template.Annotations + if changed, reason := c.compareAnnotations(curPodAnnotation, newPodAnnotation); changed { + return false, fmt.Sprintf("new job's pod template metadata annotations does not match " + reason) + } + newPgVersion := getPgVersion(new) curPgVersion := getPgVersion(cur) if newPgVersion != curPgVersion { @@ -818,6 +867,20 @@ func (c *Cluster) compareLogicalBackupJob(cur, new *batchv1.CronJob) (match bool return true, "" } +func (c *Cluster) comparePodDisruptionBudget(cur, new *policyv1.PodDisruptionBudget) (bool, string) { + //TODO: improve comparison + if !reflect.DeepEqual(new.Spec, cur.Spec) { + return false, "new PDB's spec does not match the current one" + } + if !reflect.DeepEqual(new.ObjectMeta.OwnerReferences, cur.ObjectMeta.OwnerReferences) { + return false, "new PDB's owner references do not match the current ones" + } + if changed, reason := c.compareAnnotations(cur.Annotations, new.Annotations); changed { + return false, "new PDB's annotations do not match the current ones:" + reason + } + return true, "" +} + func getPgVersion(cronJob *batchv1.CronJob) string { envs := cronJob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env for _, env := range envs { @@ -883,7 +946,6 @@ func (c *Cluster) hasFinalizer() bool { func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { updateFailed := false userInitFailed := false - syncStatefulSet := false c.mu.Lock() defer c.mu.Unlock() @@ -914,7 +976,6 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { if IsBiggerPostgresVersion(oldSpec.Spec.PostgresqlParam.PgVersion, c.GetDesiredMajorVersion()) { c.logger.Infof("postgresql version increased (%s -> %s), depending on config manual upgrade needed", oldSpec.Spec.PostgresqlParam.PgVersion, newSpec.Spec.PostgresqlParam.PgVersion) - syncStatefulSet = true } else { c.logger.Infof("postgresql major version unchanged or smaller, no changes needed") // sticking with old version, this will also advance GetDesiredVersion next time. @@ -922,12 +983,15 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { } // Service - if !reflect.DeepEqual(c.generateService(Master, &oldSpec.Spec), c.generateService(Master, &newSpec.Spec)) || - !reflect.DeepEqual(c.generateService(Replica, &oldSpec.Spec), c.generateService(Replica, &newSpec.Spec)) { - if err := c.syncServices(); err != nil { - c.logger.Errorf("could not sync services: %v", err) - updateFailed = true - } + if err := c.syncServices(); err != nil { + c.logger.Errorf("could not sync services: %v", err) + updateFailed = true + } + + // Patroni service and endpoints / config maps + if err := c.syncPatroniResources(); err != nil { + c.logger.Errorf("could not sync services: %v", err) + updateFailed = true } // Users @@ -946,7 +1010,10 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { // only when streams were not specified in oldSpec but in newSpec needStreamUser := len(oldSpec.Spec.Streams) == 0 && len(newSpec.Spec.Streams) > 0 - if !sameUsers || !sameRotatedUsers || needPoolerUser || needStreamUser { + annotationsChanged, _ := c.compareAnnotations(oldSpec.Annotations, newSpec.Annotations) + + initUsers := !sameUsers || !sameRotatedUsers || needPoolerUser || needStreamUser + if initUsers { c.logger.Debugf("initialize users") if err := c.initUsers(); err != nil { c.logger.Errorf("could not init users - skipping sync of secrets and databases: %v", err) @@ -954,7 +1021,8 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { updateFailed = true return } - + } + if initUsers || annotationsChanged { c.logger.Debugf("syncing secrets") //TODO: mind the secrets of the deleted/new users if err := c.syncSecrets(); err != nil { @@ -968,38 +1036,14 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { if c.OpConfig.StorageResizeMode != "off" { c.syncVolumes() } else { - c.logger.Infof("Storage resize is disabled (storage_resize_mode is off). Skipping volume sync.") - } - - // streams configuration - if len(oldSpec.Spec.Streams) == 0 && len(newSpec.Spec.Streams) > 0 { - syncStatefulSet = true + c.logger.Infof("Storage resize is disabled (storage_resize_mode is off). Skipping volume size sync.") } // Statefulset func() { - oldSs, err := c.generateStatefulSet(&oldSpec.Spec) - if err != nil { - c.logger.Errorf("could not generate old statefulset spec: %v", err) + if err := c.syncStatefulSet(); err != nil { + c.logger.Errorf("could not sync statefulsets: %v", err) updateFailed = true - return - } - - newSs, err := c.generateStatefulSet(&newSpec.Spec) - if err != nil { - c.logger.Errorf("could not generate new statefulset spec: %v", err) - updateFailed = true - return - } - - if syncStatefulSet || !reflect.DeepEqual(oldSs, newSs) { - c.logger.Debugf("syncing statefulsets") - syncStatefulSet = false - // TODO: avoid generating the StatefulSet object twice by passing it to syncStatefulSet - if err := c.syncStatefulSet(); err != nil { - c.logger.Errorf("could not sync statefulsets: %v", err) - updateFailed = true - } } }() @@ -1011,12 +1055,9 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { } // pod disruption budget - if oldSpec.Spec.NumberOfInstances != newSpec.Spec.NumberOfInstances { - c.logger.Debug("syncing pod disruption budgets") - if err := c.syncPodDisruptionBudget(true); err != nil { - c.logger.Errorf("could not sync pod disruption budget: %v", err) - updateFailed = true - } + if err := c.syncPodDisruptionBudget(true); err != nil { + c.logger.Errorf("could not sync pod disruption budget: %v", err) + updateFailed = true } // logical backup job @@ -1043,11 +1084,7 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { } - // apply schedule changes - // this is the only parameter of logical backups a user can overwrite in the cluster manifest - if (oldSpec.Spec.EnableLogicalBackup && newSpec.Spec.EnableLogicalBackup) && - (newSpec.Spec.LogicalBackupSchedule != oldSpec.Spec.LogicalBackupSchedule) { - c.logger.Debugf("updating schedule of the backup cron job") + if oldSpec.Spec.EnableLogicalBackup && newSpec.Spec.EnableLogicalBackup { if err := c.syncLogicalBackupJob(); err != nil { c.logger.Errorf("could not sync logical backup jobs: %v", err) updateFailed = true @@ -1091,7 +1128,7 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { } // streams - if len(newSpec.Spec.Streams) > 0 { + if len(newSpec.Spec.Streams) > 0 || len(oldSpec.Spec.Streams) != len(newSpec.Spec.Streams) { if err := c.syncStreams(); err != nil { c.logger.Errorf("could not sync streams: %v", err) updateFailed = true @@ -1171,7 +1208,6 @@ func (c *Cluster) Delete() error { } for _, role := range []PostgresRole{Master, Replica} { - if !c.patroniKubernetesUseConfigMaps() { if err := c.deleteEndpoint(role); err != nil { anyErrors = true @@ -1187,10 +1223,10 @@ func (c *Cluster) Delete() error { } } - if err := c.deletePatroniClusterObjects(); err != nil { + if err := c.deletePatroniResources(); err != nil { anyErrors = true - c.logger.Warningf("could not remove leftover patroni objects; %v", err) - c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "could not remove leftover patroni objects; %v", err) + c.logger.Warningf("could not delete all Patroni resources: %v", err) + c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "could not delete all Patroni resources: %v", err) } // Delete connection pooler objects anyway, even if it's not mentioned in the @@ -1722,96 +1758,3 @@ func (c *Cluster) Lock() { func (c *Cluster) Unlock() { c.mu.Unlock() } - -type simpleActionWithResult func() - -type clusterObjectGet func(name string) (spec.NamespacedName, error) - -type clusterObjectDelete func(name string) error - -func (c *Cluster) deletePatroniClusterObjects() error { - // TODO: figure out how to remove leftover patroni objects in other cases - var actionsList []simpleActionWithResult - - if !c.patroniUsesKubernetes() { - c.logger.Infof("not cleaning up Etcd Patroni objects on cluster delete") - } - - actionsList = append(actionsList, c.deletePatroniClusterServices) - if c.patroniKubernetesUseConfigMaps() { - actionsList = append(actionsList, c.deletePatroniClusterConfigMaps) - } else { - actionsList = append(actionsList, c.deletePatroniClusterEndpoints) - } - - c.logger.Debugf("removing leftover Patroni objects (endpoints / services and configmaps)") - for _, deleter := range actionsList { - deleter() - } - return nil -} - -func deleteClusterObject( - get clusterObjectGet, - del clusterObjectDelete, - objType string, - clusterName string, - logger *logrus.Entry) { - for _, suffix := range patroniObjectSuffixes { - name := fmt.Sprintf("%s-%s", clusterName, suffix) - - namespacedName, err := get(name) - if err == nil { - logger.Debugf("deleting %s %q", - objType, namespacedName) - - if err = del(name); err != nil { - logger.Warningf("could not delete %s %q: %v", - objType, namespacedName, err) - } - - } else if !k8sutil.ResourceNotFound(err) { - logger.Warningf("could not fetch %s %q: %v", - objType, namespacedName, err) - } - } -} - -func (c *Cluster) deletePatroniClusterServices() { - get := func(name string) (spec.NamespacedName, error) { - svc, err := c.KubeClient.Services(c.Namespace).Get(context.TODO(), name, metav1.GetOptions{}) - return util.NameFromMeta(svc.ObjectMeta), err - } - - deleteServiceFn := func(name string) error { - return c.KubeClient.Services(c.Namespace).Delete(context.TODO(), name, c.deleteOptions) - } - - deleteClusterObject(get, deleteServiceFn, "service", c.Name, c.logger) -} - -func (c *Cluster) deletePatroniClusterEndpoints() { - get := func(name string) (spec.NamespacedName, error) { - ep, err := c.KubeClient.Endpoints(c.Namespace).Get(context.TODO(), name, metav1.GetOptions{}) - return util.NameFromMeta(ep.ObjectMeta), err - } - - deleteEndpointFn := func(name string) error { - return c.KubeClient.Endpoints(c.Namespace).Delete(context.TODO(), name, c.deleteOptions) - } - - deleteClusterObject(get, deleteEndpointFn, "endpoint", c.Name, c.logger) -} - -func (c *Cluster) deletePatroniClusterConfigMaps() { - get := func(name string) (spec.NamespacedName, error) { - cm, err := c.KubeClient.ConfigMaps(c.Namespace).Get(context.TODO(), name, metav1.GetOptions{}) - return util.NameFromMeta(cm.ObjectMeta), err - } - - deleteConfigMapFn := func(name string) error { - return c.KubeClient.ConfigMaps(c.Namespace).Delete(context.TODO(), name, c.deleteOptions) - } - - deleteClusterObject(get, deleteConfigMapFn, "configmap", c.Name, c.logger) -} diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go index 9c6587746..bf3cb58ae 100644 --- a/pkg/cluster/cluster_test.go +++ b/pkg/cluster/cluster_test.go @@ -18,9 +18,11 @@ import ( "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/patroni" "github.com/zalando/postgres-operator/pkg/util/teams" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/tools/record" @@ -1361,6 +1363,23 @@ func TestCompareServices(t *testing.T) { }, } + serviceWithOwnerReference := newService( + map[string]string{ + constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", + constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, + }, + v1.ServiceTypeClusterIP, + []string{"128.141.0.0/16", "137.138.0.0/16"}) + + ownerRef := metav1.OwnerReference{ + APIVersion: "acid.zalan.do/v1", + Controller: boolToPointer(true), + Kind: "Postgresql", + Name: "clstr", + } + + serviceWithOwnerReference.ObjectMeta.OwnerReferences = append(serviceWithOwnerReference.ObjectMeta.OwnerReferences, ownerRef) + tests := []struct { about string current *v1.Service @@ -1444,203 +1463,16 @@ func TestCompareServices(t *testing.T) { reason: `new service's LoadBalancerSourceRange does not match the current one`, }, { - about: "services differ on DNS annotation", + about: "new service doesn't have owner references", current: newService( map[string]string{ constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, }, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - new: newService( - map[string]string{ - constants.ZalandoDNSNameAnnotation: "new_clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, - }, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - match: false, - reason: `new service's annotations does not match the current one: "external-dns.alpha.kubernetes.io/hostname" changed from "clstr.acid.zalan.do" to "new_clstr.acid.zalan.do".`, - }, - { - about: "services differ on AWS ELB annotation", - current: newService( - map[string]string{ - constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, - }, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - new: newService( - map[string]string{ - constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: "1800", - }, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - match: false, - reason: `new service's annotations does not match the current one: "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout" changed from "3600" to "1800".`, - }, - { - about: "service changes existing annotation", - current: newService( - map[string]string{ - constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, - "foo": "bar", - }, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - new: newService( - map[string]string{ - constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, - "foo": "baz", - }, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - match: false, - reason: `new service's annotations does not match the current one: "foo" changed from "bar" to "baz".`, - }, - { - about: "service changes multiple existing annotations", - current: newService( - map[string]string{ - constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, - "foo": "bar", - "bar": "foo", - }, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - new: newService( - map[string]string{ - constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, - "foo": "baz", - "bar": "fooz", - }, - v1.ServiceTypeLoadBalancer, + v1.ServiceTypeClusterIP, []string{"128.141.0.0/16", "137.138.0.0/16"}), + new: serviceWithOwnerReference, match: false, - // Test just the prefix to avoid flakiness and map sorting - reason: `new service's annotations does not match the current one:`, - }, - { - about: "service adds a new custom annotation", - current: newService( - map[string]string{ - constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, - }, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - new: newService( - map[string]string{ - constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, - "foo": "bar", - }, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - match: false, - reason: `new service's annotations does not match the current one: Added "foo" with value "bar".`, - }, - { - about: "service removes a custom annotation", - current: newService( - map[string]string{ - constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, - "foo": "bar", - }, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - new: newService( - map[string]string{ - constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, - }, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - match: false, - reason: `new service's annotations does not match the current one: Removed "foo".`, - }, - { - about: "service removes a custom annotation and adds a new one", - current: newService( - map[string]string{ - constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, - "foo": "bar", - }, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - new: newService( - map[string]string{ - constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, - "bar": "foo", - }, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - match: false, - reason: `new service's annotations does not match the current one: Removed "foo". Added "bar" with value "foo".`, - }, - { - about: "service removes a custom annotation, adds a new one and change another", - current: newService( - map[string]string{ - constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, - "foo": "bar", - "zalan": "do", - }, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - new: newService( - map[string]string{ - constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, - "bar": "foo", - "zalan": "do.com", - }, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - match: false, - // Test just the prefix to avoid flakiness and map sorting - reason: `new service's annotations does not match the current one: Removed "foo".`, - }, - { - about: "service add annotations", - current: newService( - map[string]string{}, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - new: newService( - map[string]string{ - constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do", - constants.ElbTimeoutAnnotationName: constants.ElbTimeoutAnnotationValue, - }, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - match: false, - // Test just the prefix to avoid flakiness and map sorting - reason: `new service's annotations does not match the current one: Added `, - }, - { - about: "ignored annotations", - current: newService( - map[string]string{}, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - new: newService( - map[string]string{ - "k8s.v1.cni.cncf.io/network-status": "up", - }, - v1.ServiceTypeLoadBalancer, - []string{"128.141.0.0/16", "137.138.0.0/16"}), - match: true, }, } @@ -1663,7 +1495,7 @@ func TestCompareServices(t *testing.T) { } } -func newCronJob(image, schedule string, vars []v1.EnvVar) *batchv1.CronJob { +func newCronJob(image, schedule string, vars []v1.EnvVar, mounts []v1.VolumeMount) *batchv1.CronJob { cron := &batchv1.CronJob{ Spec: batchv1.CronJobSpec{ Schedule: schedule, @@ -1676,6 +1508,37 @@ func newCronJob(image, schedule string, vars []v1.EnvVar) *batchv1.CronJob { Name: "logical-backup", Image: image, Env: vars, + Ports: []v1.ContainerPort{ + { + ContainerPort: patroni.ApiPort, + Protocol: v1.ProtocolTCP, + }, + { + ContainerPort: pgPort, + Protocol: v1.ProtocolTCP, + }, + { + ContainerPort: operatorPort, + Protocol: v1.ProtocolTCP, + }, + }, + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("100m"), + v1.ResourceMemory: resource.MustParse("100Mi"), + }, + Limits: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("100m"), + v1.ResourceMemory: resource.MustParse("100Mi"), + }, + }, + SecurityContext: &v1.SecurityContext{ + AllowPrivilegeEscalation: nil, + Privileged: util.False(), + ReadOnlyRootFilesystem: util.False(), + Capabilities: nil, + }, + VolumeMounts: mounts, }, }, }, @@ -1692,37 +1555,110 @@ func TestCompareLogicalBackupJob(t *testing.T) { img1 := "registry.opensource.zalan.do/acid/logical-backup:v1.0" img2 := "registry.opensource.zalan.do/acid/logical-backup:v2.0" + clientSet := fake.NewSimpleClientset() + acidClientSet := fakeacidv1.NewSimpleClientset() + namespace := "default" + + client := k8sutil.KubernetesClient{ + CronJobsGetter: clientSet.BatchV1(), + PostgresqlsGetter: acidClientSet.AcidV1(), + } + pg := acidv1.Postgresql{ + ObjectMeta: metav1.ObjectMeta{ + Name: "acid-cron-cluster", + Namespace: namespace, + }, + Spec: acidv1.PostgresSpec{ + Volume: acidv1.Volume{ + Size: "1Gi", + }, + EnableLogicalBackup: true, + LogicalBackupSchedule: "0 0 * * *", + LogicalBackupRetention: "3 months", + }, + } + + var cluster = New( + Config{ + OpConfig: config.Config{ + 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", + }, + LogicalBackup: config.LogicalBackup{ + LogicalBackupSchedule: "30 00 * * *", + LogicalBackupDockerImage: img1, + LogicalBackupJobPrefix: "logical-backup-", + LogicalBackupCPURequest: "100m", + LogicalBackupCPULimit: "100m", + LogicalBackupMemoryRequest: "100Mi", + LogicalBackupMemoryLimit: "100Mi", + LogicalBackupProvider: "s3", + LogicalBackupS3Bucket: "testBucket", + LogicalBackupS3BucketPrefix: "spilo", + LogicalBackupS3Region: "eu-central-1", + LogicalBackupS3Endpoint: "https://s3.amazonaws.com", + LogicalBackupS3AccessKeyID: "access", + LogicalBackupS3SecretAccessKey: "secret", + LogicalBackupS3SSE: "aws:kms", + LogicalBackupS3RetentionTime: "3 months", + LogicalBackupCronjobEnvironmentSecret: "", + }, + }, + }, client, pg, logger, eventRecorder) + + desiredCronJob, err := cluster.generateLogicalBackupJob() + if err != nil { + t.Errorf("Could not generate logical backup job with error: %v", err) + } + + err = cluster.createLogicalBackupJob() + if err != nil { + t.Errorf("Could not create logical backup job with error: %v", err) + } + + currentCronJob, err := cluster.KubeClient.CronJobs(namespace).Get(context.TODO(), cluster.getLogicalBackupJobName(), metav1.GetOptions{}) + if err != nil { + t.Errorf("Could not create logical backup job with error: %v", err) + } + tests := []struct { about string - current *batchv1.CronJob - new *batchv1.CronJob + cronjob *batchv1.CronJob match bool reason string }{ { about: "two equal cronjobs", - current: newCronJob(img1, "0 0 * * *", []v1.EnvVar{}), - new: newCronJob(img1, "0 0 * * *", []v1.EnvVar{}), + cronjob: newCronJob(img1, "0 0 * * *", []v1.EnvVar{}, []v1.VolumeMount{}), match: true, }, { about: "two cronjobs with different image", - current: newCronJob(img1, "0 0 * * *", []v1.EnvVar{}), - new: newCronJob(img2, "0 0 * * *", []v1.EnvVar{}), + cronjob: newCronJob(img2, "0 0 * * *", []v1.EnvVar{}, []v1.VolumeMount{}), match: false, reason: fmt.Sprintf("new job's image %q does not match the current one %q", img2, img1), }, { about: "two cronjobs with different schedule", - current: newCronJob(img1, "0 0 * * *", []v1.EnvVar{}), - new: newCronJob(img1, "0 * * * *", []v1.EnvVar{}), + cronjob: newCronJob(img1, "0 * * * *", []v1.EnvVar{}, []v1.VolumeMount{}), match: false, reason: fmt.Sprintf("new job's schedule %q does not match the current one %q", "0 * * * *", "0 0 * * *"), }, + { + about: "two cronjobs with empty and nil volume mounts", + cronjob: newCronJob(img1, "0 0 * * *", []v1.EnvVar{}, nil), + match: true, + }, { about: "two cronjobs with different environment variables", - current: newCronJob(img1, "0 0 * * *", []v1.EnvVar{{Name: "LOGICAL_BACKUP_S3_BUCKET_PREFIX", Value: "spilo"}}), - new: newCronJob(img1, "0 0 * * *", []v1.EnvVar{{Name: "LOGICAL_BACKUP_S3_BUCKET_PREFIX", Value: "logical-backup"}}), + cronjob: newCronJob(img1, "0 0 * * *", []v1.EnvVar{{Name: "LOGICAL_BACKUP_S3_BUCKET_PREFIX", Value: "logical-backup"}}, []v1.VolumeMount{}), match: false, reason: "logical backup container specs do not match: new cronjob container's logical-backup (index 0) environment does not match the current one", }, @@ -1730,9 +1666,21 @@ func TestCompareLogicalBackupJob(t *testing.T) { for _, tt := range tests { t.Run(tt.about, func(t *testing.T) { - match, reason := cl.compareLogicalBackupJob(tt.current, tt.new) + desiredCronJob.Spec.Schedule = tt.cronjob.Spec.Schedule + desiredCronJob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Image = tt.cronjob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Image + desiredCronJob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].VolumeMounts = tt.cronjob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].VolumeMounts + + for _, testEnv := range tt.cronjob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env { + for i, env := range desiredCronJob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env { + if env.Name == testEnv.Name { + desiredCronJob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env[i] = testEnv + } + } + } + + match, reason := cluster.compareLogicalBackupJob(currentCronJob, desiredCronJob) if match != tt.match { - t.Errorf("%s - unexpected match result %t when comparing cronjobs %q and %q", t.Name(), match, tt.current, tt.new) + t.Errorf("%s - unexpected match result %t when comparing cronjobs %#v and %#v", t.Name(), match, currentCronJob, desiredCronJob) } else { if !strings.HasPrefix(reason, tt.reason) { t.Errorf("%s - expected reason prefix %s, found %s", t.Name(), tt.reason, reason) @@ -1927,3 +1875,183 @@ func TestComparePorts(t *testing.T) { }) } } + +func TestCompareVolumeMounts(t *testing.T) { + testCases := []struct { + name string + mountsA []v1.VolumeMount + mountsB []v1.VolumeMount + expected bool + }{ + { + name: "empty vs nil", + mountsA: []v1.VolumeMount{}, + mountsB: nil, + expected: true, + }, + { + name: "both empty", + mountsA: []v1.VolumeMount{}, + mountsB: []v1.VolumeMount{}, + expected: true, + }, + { + name: "same mounts", + mountsA: []v1.VolumeMount{ + { + Name: "data", + ReadOnly: false, + MountPath: "/data", + SubPath: "subdir", + }, + }, + mountsB: []v1.VolumeMount{ + { + Name: "data", + ReadOnly: false, + MountPath: "/data", + SubPath: "subdir", + }, + }, + expected: true, + }, + { + name: "different mounts", + mountsA: []v1.VolumeMount{ + { + Name: "data", + ReadOnly: false, + MountPath: "/data", + SubPathExpr: "$(POD_NAME)", + }, + }, + mountsB: []v1.VolumeMount{ + { + Name: "data", + ReadOnly: false, + MountPath: "/data", + SubPath: "subdir", + }, + }, + expected: false, + }, + { + name: "one equal mount one different", + mountsA: []v1.VolumeMount{ + { + Name: "data", + ReadOnly: false, + MountPath: "/data", + SubPath: "subdir", + }, + { + Name: "poddata", + ReadOnly: false, + MountPath: "/poddata", + SubPathExpr: "$(POD_NAME)", + }, + }, + mountsB: []v1.VolumeMount{ + { + Name: "data", + ReadOnly: false, + MountPath: "/data", + SubPath: "subdir", + }, + { + Name: "etc", + ReadOnly: true, + MountPath: "/etc", + }, + }, + expected: false, + }, + { + name: "same mounts, different order", + mountsA: []v1.VolumeMount{ + { + Name: "data", + ReadOnly: false, + MountPath: "/data", + SubPath: "subdir", + }, + { + Name: "etc", + ReadOnly: true, + MountPath: "/etc", + }, + }, + mountsB: []v1.VolumeMount{ + { + Name: "etc", + ReadOnly: true, + MountPath: "/etc", + }, + { + Name: "data", + ReadOnly: false, + MountPath: "/data", + SubPath: "subdir", + }, + }, + expected: true, + }, + { + name: "new mounts added", + mountsA: []v1.VolumeMount{ + { + Name: "data", + ReadOnly: false, + MountPath: "/data", + SubPath: "subdir", + }, + }, + mountsB: []v1.VolumeMount{ + { + Name: "etc", + ReadOnly: true, + MountPath: "/etc", + }, + { + Name: "data", + ReadOnly: false, + MountPath: "/data", + SubPath: "subdir", + }, + }, + expected: false, + }, + { + name: "one mount removed", + mountsA: []v1.VolumeMount{ + { + Name: "data", + ReadOnly: false, + MountPath: "/data", + SubPath: "subdir", + }, + { + Name: "etc", + ReadOnly: true, + MountPath: "/etc", + }, + }, + mountsB: []v1.VolumeMount{ + { + Name: "data", + ReadOnly: false, + MountPath: "/data", + SubPath: "subdir", + }, + }, + expected: false, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := compareVolumeMounts(tt.mountsA, tt.mountsB) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/cluster/connection_pooler.go b/pkg/cluster/connection_pooler.go index 7a97497d7..25d4514d1 100644 --- a/pkg/cluster/connection_pooler.go +++ b/pkg/cluster/connection_pooler.go @@ -3,6 +3,7 @@ package cluster import ( "context" "fmt" + "reflect" "strings" "time" @@ -654,7 +655,7 @@ func (c *Cluster) deleteConnectionPoolerSecret() (err error) { if err != nil { c.logger.Debugf("could not get connection pooler secret %s: %v", secretName, err) } else { - if err = c.deleteSecret(secret.UID, *secret); err != nil { + if err = c.deleteSecret(secret.UID); err != nil { return fmt.Errorf("could not delete pooler secret: %v", err) } } @@ -663,11 +664,19 @@ func (c *Cluster) deleteConnectionPoolerSecret() (err error) { // Perform actual patching of a connection pooler deployment, assuming that all // the check were already done before. -func updateConnectionPoolerDeployment(KubeClient k8sutil.KubernetesClient, newDeployment *appsv1.Deployment) (*appsv1.Deployment, error) { +func updateConnectionPoolerDeployment(KubeClient k8sutil.KubernetesClient, newDeployment *appsv1.Deployment, doUpdate bool) (*appsv1.Deployment, error) { if newDeployment == nil { return nil, fmt.Errorf("there is no connection pooler in the cluster") } + if doUpdate { + updatedDeployment, err := KubeClient.Deployments(newDeployment.Namespace).Update(context.TODO(), newDeployment, metav1.UpdateOptions{}) + if err != nil { + return nil, fmt.Errorf("could not update pooler deployment to match desired state: %v", err) + } + return updatedDeployment, nil + } + patchData, err := specPatch(newDeployment.Spec) if err != nil { return nil, fmt.Errorf("could not form patch for the connection pooler deployment: %v", err) @@ -691,8 +700,8 @@ func updateConnectionPoolerDeployment(KubeClient k8sutil.KubernetesClient, newDe return deployment, nil } -// updateConnectionPoolerAnnotations updates the annotations of connection pooler deployment -func updateConnectionPoolerAnnotations(KubeClient k8sutil.KubernetesClient, deployment *appsv1.Deployment, annotations map[string]string) (*appsv1.Deployment, error) { +// patchConnectionPoolerAnnotations updates the annotations of connection pooler deployment +func patchConnectionPoolerAnnotations(KubeClient k8sutil.KubernetesClient, deployment *appsv1.Deployment, annotations map[string]string) (*appsv1.Deployment, error) { patchData, err := metaAnnotationsPatch(annotations) if err != nil { return nil, fmt.Errorf("could not form patch for the connection pooler deployment metadata: %v", err) @@ -751,6 +760,7 @@ func (c *Cluster) needSyncConnectionPoolerDefaults(Config *Config, spec *acidv1. if spec == nil { spec = &acidv1.ConnectionPooler{} } + if spec.NumberOfInstances == nil && *deployment.Spec.Replicas != *config.NumberOfInstances { @@ -1014,18 +1024,30 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql newConnectionPooler = &acidv1.ConnectionPooler{} } - var specSync bool + var specSync, updateDeployment bool var specReason []string + if !reflect.DeepEqual(deployment.ObjectMeta.OwnerReferences, c.ownerReferences()) { + c.logger.Info("new connection pooler owner references do not match the current ones") + updateDeployment = true + } + if oldSpec != nil { specSync, specReason = needSyncConnectionPoolerSpecs(oldConnectionPooler, newConnectionPooler, c.logger) syncReason = append(syncReason, specReason...) } + newPodAnnotations := c.annotationsSet(c.generatePodAnnotations(&c.Spec)) + if changed, reason := c.compareAnnotations(deployment.Spec.Template.Annotations, newPodAnnotations); changed { + specSync = true + syncReason = append(syncReason, []string{"new connection pooler's pod template annotations do not match the current ones: " + reason}...) + deployment.Spec.Template.Annotations = newPodAnnotations + } + defaultsSync, defaultsReason := c.needSyncConnectionPoolerDefaults(&c.Config, newConnectionPooler, deployment) syncReason = append(syncReason, defaultsReason...) - if specSync || defaultsSync { + if specSync || defaultsSync || updateDeployment { c.logger.Infof("update connection pooler deployment %s, reason: %+v", c.connectionPoolerName(role), syncReason) newDeployment, err = c.generateConnectionPoolerDeployment(c.ConnectionPooler[role]) @@ -1033,22 +1055,22 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql return syncReason, fmt.Errorf("could not generate deployment for connection pooler: %v", err) } - deployment, err = updateConnectionPoolerDeployment(c.KubeClient, newDeployment) + deployment, err = updateConnectionPoolerDeployment(c.KubeClient, newDeployment, updateDeployment) if err != nil { return syncReason, err } c.ConnectionPooler[role].Deployment = deployment } - } - newAnnotations := c.AnnotationsToPropagate(c.annotationsSet(c.ConnectionPooler[role].Deployment.Annotations)) - if newAnnotations != nil { - deployment, err = updateConnectionPoolerAnnotations(c.KubeClient, c.ConnectionPooler[role].Deployment, newAnnotations) - if err != nil { - return nil, err + newAnnotations := c.AnnotationsToPropagate(c.annotationsSet(nil)) // including the downscaling annotations + if changed, _ := c.compareAnnotations(deployment.Annotations, newAnnotations); changed { + deployment, err = patchConnectionPoolerAnnotations(c.KubeClient, deployment, newAnnotations) + if err != nil { + return nil, err + } + c.ConnectionPooler[role].Deployment = deployment } - c.ConnectionPooler[role].Deployment = deployment } // check if pooler pods must be replaced due to secret update @@ -1076,22 +1098,26 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql if err != nil { return nil, fmt.Errorf("could not delete pooler pod: %v", err) } + } else if changed, _ := c.compareAnnotations(pod.Annotations, deployment.Spec.Template.Annotations); changed { + patchData, err := metaAnnotationsPatch(deployment.Spec.Template.Annotations) + if err != nil { + return nil, fmt.Errorf("could not form patch for pooler's pod annotations: %v", err) + } + _, err = c.KubeClient.Pods(pod.Namespace).Patch(context.TODO(), pod.Name, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}) + if err != nil { + return nil, fmt.Errorf("could not patch annotations for pooler's pod %q: %v", pod.Name, err) + } } } if service, err = c.KubeClient.Services(c.Namespace).Get(context.TODO(), c.connectionPoolerName(role), metav1.GetOptions{}); err == nil { c.ConnectionPooler[role].Service = service desiredSvc := c.generateConnectionPoolerService(c.ConnectionPooler[role]) - if match, reason := c.compareServices(service, desiredSvc); !match { - syncReason = append(syncReason, reason) - c.logServiceChanges(role, service, desiredSvc, false, reason) - newService, err = c.updateService(role, service, desiredSvc) - if err != nil { - return syncReason, fmt.Errorf("could not update %s service to match desired state: %v", role, err) - } - c.ConnectionPooler[role].Service = newService - c.logger.Infof("%s service %q is in the desired state now", role, util.NameFromMeta(desiredSvc.ObjectMeta)) + newService, err = c.updateService(role, service, desiredSvc) + if err != nil { + return syncReason, fmt.Errorf("could not update %s service to match desired state: %v", role, err) } + c.ConnectionPooler[role].Service = newService return NoSync, nil } diff --git a/pkg/cluster/connection_pooler_test.go b/pkg/cluster/connection_pooler_test.go index f7f2e2cb0..e6472d017 100644 --- a/pkg/cluster/connection_pooler_test.go +++ b/pkg/cluster/connection_pooler_test.go @@ -1077,6 +1077,9 @@ func TestConnectionPoolerServiceSpec(t *testing.T) { ConnectionPoolerDefaultMemoryRequest: "100Mi", ConnectionPoolerDefaultMemoryLimit: "100Mi", }, + Resources: config.Resources{ + EnableOwnerReferences: util.True(), + }, }, }, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder) cluster.Statefulset = &appsv1.StatefulSet{ diff --git a/pkg/cluster/database.go b/pkg/cluster/database.go index cc203eef5..094af4aca 100644 --- a/pkg/cluster/database.go +++ b/pkg/cluster/database.go @@ -46,12 +46,15 @@ const ( createExtensionSQL = `CREATE EXTENSION IF NOT EXISTS "%s" SCHEMA "%s"` alterExtensionSQL = `ALTER EXTENSION "%s" SET SCHEMA "%s"` - getPublicationsSQL = `SELECT p.pubname, string_agg(pt.schemaname || '.' || pt.tablename, ', ' ORDER BY pt.schemaname, pt.tablename) + getPublicationsSQL = `SELECT p.pubname, COALESCE(string_agg(pt.schemaname || '.' || pt.tablename, ', ' ORDER BY pt.schemaname, pt.tablename), '') AS pubtables FROM pg_publication p LEFT JOIN pg_publication_tables pt ON pt.pubname = p.pubname + WHERE p.pubowner = 'postgres'::regrole + AND p.pubname LIKE 'fes_%' GROUP BY p.pubname;` createPublicationSQL = `CREATE PUBLICATION "%s" FOR TABLE %s WITH (publish = 'insert, update');` alterPublicationSQL = `ALTER PUBLICATION "%s" SET TABLE %s;` + dropPublicationSQL = `DROP PUBLICATION "%s";` globalDefaultPrivilegesSQL = `SET ROLE TO "%s"; ALTER DEFAULT PRIVILEGES GRANT USAGE ON SCHEMAS TO "%s","%s"; @@ -628,6 +631,14 @@ func (c *Cluster) getPublications() (publications map[string]string, err error) return dbPublications, err } +func (c *Cluster) executeDropPublication(pubName string) error { + c.logger.Infof("dropping publication %q", pubName) + if _, err := c.pgDb.Exec(fmt.Sprintf(dropPublicationSQL, pubName)); err != nil { + return fmt.Errorf("could not execute drop publication: %v", err) + } + return nil +} + // executeCreatePublication creates new publication for given tables // The caller is responsible for opening and closing the database connection. func (c *Cluster) executeCreatePublication(pubName, tableList string) error { diff --git a/pkg/cluster/exec.go b/pkg/cluster/exec.go index 8b5089b4e..5605a70f6 100644 --- a/pkg/cluster/exec.go +++ b/pkg/cluster/exec.go @@ -15,7 +15,7 @@ import ( "github.com/zalando/postgres-operator/pkg/util/constants" ) -//ExecCommand executes arbitrary command inside the pod +// ExecCommand executes arbitrary command inside the pod func (c *Cluster) ExecCommand(podName *spec.NamespacedName, command ...string) (string, error) { c.setProcessName("executing command %q", strings.Join(command, " ")) @@ -59,7 +59,7 @@ func (c *Cluster) ExecCommand(podName *spec.NamespacedName, command ...string) ( return "", fmt.Errorf("failed to init executor: %v", err) } - err = exec.Stream(remotecommand.StreamOptions{ + err = exec.StreamWithContext(context.TODO(), remotecommand.StreamOptions{ Stdout: &execOut, Stderr: &execErr, Tty: false, diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 8a86de8c1..8934b6b49 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -47,11 +47,6 @@ const ( operatorPort = 8080 ) -type pgUser struct { - Password string `json:"password"` - Options []string `json:"options"` -} - type patroniDCS struct { TTL uint32 `json:"ttl,omitempty"` LoopWait uint32 `json:"loop_wait,omitempty"` @@ -79,19 +74,13 @@ func (c *Cluster) statefulSetName() string { return c.Name } -func (c *Cluster) endpointName(role PostgresRole) string { - name := c.Name - if role == Replica { - name = fmt.Sprintf("%s-%s", name, "repl") - } - - return name -} - func (c *Cluster) serviceName(role PostgresRole) string { name := c.Name - if role == Replica { + switch role { + case Replica: name = fmt.Sprintf("%s-%s", name, "repl") + case Patroni: + name = fmt.Sprintf("%s-%s", name, "config") } return name @@ -892,7 +881,7 @@ func (c *Cluster) generatePodTemplate( addSecretVolume(&podSpec, additionalSecretMount, additionalSecretMountPath) } - if additionalVolumes != nil { + if len(additionalVolumes) > 0 { c.addAdditionalVolumes(&podSpec, additionalVolumes) } @@ -1530,10 +1519,11 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef statefulSet := &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ - Name: c.statefulSetName(), - Namespace: c.Namespace, - Labels: c.labelsSet(true), - Annotations: c.AnnotationsToPropagate(c.annotationsSet(nil)), + Name: c.statefulSetName(), + Namespace: c.Namespace, + Labels: c.labelsSet(true), + Annotations: c.AnnotationsToPropagate(c.annotationsSet(nil)), + OwnerReferences: c.ownerReferences(), }, Spec: appsv1.StatefulSetSpec{ Replicas: &numberOfInstances, @@ -1929,12 +1919,21 @@ func (c *Cluster) generateSingleUserSecret(namespace string, pgUser spec.PgUser) lbls = c.connectionPoolerLabels("", false).MatchLabels } + // if secret lives in another namespace we cannot set ownerReferences + var ownerReferences []metav1.OwnerReference + if c.Config.OpConfig.EnableCrossNamespaceSecret && strings.Contains(username, ".") { + ownerReferences = nil + } else { + ownerReferences = c.ownerReferences() + } + secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: c.credentialSecretName(username), - Namespace: pgUser.Namespace, - Labels: lbls, - Annotations: c.annotationsSet(nil), + Name: c.credentialSecretName(username), + Namespace: pgUser.Namespace, + Labels: lbls, + Annotations: c.annotationsSet(nil), + OwnerReferences: ownerReferences, }, Type: v1.SecretTypeOpaque, Data: map[string][]byte{ @@ -1992,10 +1991,11 @@ func (c *Cluster) generateService(role PostgresRole, spec *acidv1.PostgresSpec) service := &v1.Service{ ObjectMeta: metav1.ObjectMeta{ - Name: c.serviceName(role), - Namespace: c.Namespace, - Labels: c.roleLabelsSet(true, role), - Annotations: c.annotationsSet(c.generateServiceAnnotations(role, spec)), + Name: c.serviceName(role), + Namespace: c.Namespace, + Labels: c.roleLabelsSet(true, role), + Annotations: c.annotationsSet(c.generateServiceAnnotations(role, spec)), + OwnerReferences: c.ownerReferences(), }, Spec: serviceSpec, } @@ -2061,9 +2061,11 @@ func (c *Cluster) getCustomServiceAnnotations(role PostgresRole, spec *acidv1.Po func (c *Cluster) generateEndpoint(role PostgresRole, subsets []v1.EndpointSubset) *v1.Endpoints { endpoints := &v1.Endpoints{ ObjectMeta: metav1.ObjectMeta{ - Name: c.endpointName(role), - Namespace: c.Namespace, - Labels: c.roleLabelsSet(true, role), + Name: c.serviceName(role), + Namespace: c.Namespace, + Annotations: c.annotationsSet(nil), + Labels: c.roleLabelsSet(true, role), + OwnerReferences: c.ownerReferences(), }, } if len(subsets) > 0 { @@ -2224,10 +2226,11 @@ func (c *Cluster) generatePodDisruptionBudget() *policyv1.PodDisruptionBudget { return &policyv1.PodDisruptionBudget{ ObjectMeta: metav1.ObjectMeta{ - Name: c.podDisruptionBudgetName(), - Namespace: c.Namespace, - Labels: c.labelsSet(true), - Annotations: c.annotationsSet(nil), + Name: c.podDisruptionBudgetName(), + Namespace: c.Namespace, + Labels: c.labelsSet(true), + Annotations: c.annotationsSet(nil), + OwnerReferences: c.ownerReferences(), }, Spec: policyv1.PodDisruptionBudgetSpec{ MinAvailable: &minAvailable, @@ -2360,10 +2363,11 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) { cronJob := &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ - Name: c.getLogicalBackupJobName(), - Namespace: c.Namespace, - Labels: c.labelsSet(true), - Annotations: c.annotationsSet(nil), + Name: c.getLogicalBackupJobName(), + Namespace: c.Namespace, + Labels: c.labelsSet(true), + Annotations: c.annotationsSet(nil), + OwnerReferences: c.ownerReferences(), }, Spec: batchv1.CronJobSpec{ Schedule: schedule, @@ -2477,7 +2481,9 @@ func (c *Cluster) generateLogicalBackupPodEnvVars() []v1.EnvVar { } case "gcs": - envVars = append(envVars, v1.EnvVar{Name: "LOGICAL_BACKUP_GOOGLE_APPLICATION_CREDENTIALS", Value: c.OpConfig.LogicalBackup.LogicalBackupGoogleApplicationCredentials}) + if c.OpConfig.LogicalBackup.LogicalBackupGoogleApplicationCredentials != "" { + envVars = append(envVars, v1.EnvVar{Name: "LOGICAL_BACKUP_GOOGLE_APPLICATION_CREDENTIALS", Value: c.OpConfig.LogicalBackup.LogicalBackupGoogleApplicationCredentials}) + } case "az": envVars = appendEnvVars(envVars, []v1.EnvVar{ @@ -2488,11 +2494,11 @@ func (c *Cluster) generateLogicalBackupPodEnvVars() []v1.EnvVar { { Name: "LOGICAL_BACKUP_AZURE_STORAGE_CONTAINER", Value: c.OpConfig.LogicalBackup.LogicalBackupAzureStorageContainer, - }, - { - Name: "LOGICAL_BACKUP_AZURE_STORAGE_ACCOUNT_KEY", - Value: c.OpConfig.LogicalBackup.LogicalBackupAzureStorageAccountKey, }}...) + + if c.OpConfig.LogicalBackup.LogicalBackupAzureStorageAccountKey != "" { + envVars = append(envVars, v1.EnvVar{Name: "LOGICAL_BACKUP_AZURE_STORAGE_ACCOUNT_KEY", Value: c.OpConfig.LogicalBackup.LogicalBackupAzureStorageAccountKey}) + } } return envVars @@ -2518,22 +2524,26 @@ func (c *Cluster) getLogicalBackupJobName() (jobName string) { // survived, we can't delete an object because it will affect the functioning // cluster). func (c *Cluster) ownerReferences() []metav1.OwnerReference { - controller := true - - if c.Statefulset == nil { - c.logger.Warning("Cannot get owner reference, no statefulset") - return []metav1.OwnerReference{} + currentOwnerReferences := c.ObjectMeta.OwnerReferences + if c.OpConfig.EnableOwnerReferences == nil || !*c.OpConfig.EnableOwnerReferences { + return currentOwnerReferences } - return []metav1.OwnerReference{ - { - UID: c.Statefulset.ObjectMeta.UID, - APIVersion: "apps/v1", - Kind: "StatefulSet", - Name: c.Statefulset.ObjectMeta.Name, - Controller: &controller, - }, + for _, ownerRef := range currentOwnerReferences { + if ownerRef.UID == c.Postgresql.ObjectMeta.UID { + return currentOwnerReferences + } } + + controllerReference := metav1.OwnerReference{ + UID: c.Postgresql.ObjectMeta.UID, + APIVersion: acidv1.SchemeGroupVersion.Identifier(), + Kind: acidv1.PostgresCRDResourceKind, + Name: c.Postgresql.ObjectMeta.Name, + Controller: util.True(), + } + + return append(currentOwnerReferences, controllerReference) } func ensurePath(file string, defaultDir string, defaultFile string) string { diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index 2eeefb218..f18861687 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -1566,22 +1566,28 @@ func TestPodAffinity(t *testing.T) { } func testDeploymentOwnerReference(cluster *Cluster, deployment *appsv1.Deployment) error { + if len(deployment.ObjectMeta.OwnerReferences) == 0 { + return nil + } owner := deployment.ObjectMeta.OwnerReferences[0] - if owner.Name != cluster.Statefulset.ObjectMeta.Name { - return fmt.Errorf("Ownere reference is incorrect, got %s, expected %s", - owner.Name, cluster.Statefulset.ObjectMeta.Name) + if owner.Name != cluster.Postgresql.ObjectMeta.Name { + return fmt.Errorf("Owner reference is incorrect, got %s, expected %s", + owner.Name, cluster.Postgresql.ObjectMeta.Name) } return nil } func testServiceOwnerReference(cluster *Cluster, service *v1.Service, role PostgresRole) error { + if len(service.ObjectMeta.OwnerReferences) == 0 { + return nil + } owner := service.ObjectMeta.OwnerReferences[0] - if owner.Name != cluster.Statefulset.ObjectMeta.Name { - return fmt.Errorf("Ownere reference is incorrect, got %s, expected %s", - owner.Name, cluster.Statefulset.ObjectMeta.Name) + if owner.Name != cluster.Postgresql.ObjectMeta.Name { + return fmt.Errorf("Owner reference is incorrect, got %s, expected %s", + owner.Name, cluster.Postgresql.ObjectMeta.Name) } return nil @@ -2320,13 +2326,69 @@ func TestSidecars(t *testing.T) { } func TestGeneratePodDisruptionBudget(t *testing.T) { + testName := "Test PodDisruptionBudget spec generation" + + hasName := func(pdbName string) func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error { + return func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error { + if pdbName != podDisruptionBudget.ObjectMeta.Name { + return fmt.Errorf("PodDisruptionBudget name is incorrect, got %s, expected %s", + podDisruptionBudget.ObjectMeta.Name, pdbName) + } + return nil + } + } + + hasMinAvailable := func(expectedMinAvailable int) func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error { + return func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error { + actual := podDisruptionBudget.Spec.MinAvailable.IntVal + if actual != int32(expectedMinAvailable) { + return fmt.Errorf("PodDisruptionBudget MinAvailable is incorrect, got %d, expected %d", + actual, expectedMinAvailable) + } + return nil + } + } + + testLabelsAndSelectors := func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error { + masterLabelSelectorDisabled := cluster.OpConfig.PDBMasterLabelSelector != nil && !*cluster.OpConfig.PDBMasterLabelSelector + if podDisruptionBudget.ObjectMeta.Namespace != "myapp" { + return fmt.Errorf("Object Namespace incorrect.") + } + if !reflect.DeepEqual(podDisruptionBudget.Labels, map[string]string{"team": "myapp", "cluster-name": "myapp-database"}) { + return fmt.Errorf("Labels incorrect.") + } + if !masterLabelSelectorDisabled && + !reflect.DeepEqual(podDisruptionBudget.Spec.Selector, &metav1.LabelSelector{ + MatchLabels: map[string]string{"spilo-role": "master", "cluster-name": "myapp-database"}}) { + + return fmt.Errorf("MatchLabels incorrect.") + } + + return nil + } + + testPodDisruptionBudgetOwnerReference := func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error { + if len(podDisruptionBudget.ObjectMeta.OwnerReferences) == 0 { + return nil + } + owner := podDisruptionBudget.ObjectMeta.OwnerReferences[0] + + if owner.Name != cluster.Postgresql.ObjectMeta.Name { + return fmt.Errorf("Owner reference is incorrect, got %s, expected %s", + owner.Name, cluster.Postgresql.ObjectMeta.Name) + } + + return nil + } + tests := []struct { - c *Cluster - out policyv1.PodDisruptionBudget + scenario string + spec *Cluster + check []func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error }{ - // With multiple instances. { - New( + scenario: "With multiple instances", + spec: New( Config{OpConfig: config.Config{Resources: config.Resources{ClusterNameLabel: "cluster-name", PodRoleLabel: "spilo-role"}, PDBNameFormat: "postgres-{cluster}-pdb"}}, k8sutil.KubernetesClient{}, acidv1.Postgresql{ @@ -2334,23 +2396,16 @@ func TestGeneratePodDisruptionBudget(t *testing.T) { Spec: acidv1.PostgresSpec{TeamID: "myapp", NumberOfInstances: 3}}, logger, eventRecorder), - policyv1.PodDisruptionBudget{ - ObjectMeta: metav1.ObjectMeta{ - Name: "postgres-myapp-database-pdb", - Namespace: "myapp", - Labels: map[string]string{"team": "myapp", "cluster-name": "myapp-database"}, - }, - Spec: policyv1.PodDisruptionBudgetSpec{ - MinAvailable: util.ToIntStr(1), - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"spilo-role": "master", "cluster-name": "myapp-database"}, - }, - }, + check: []func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error{ + testPodDisruptionBudgetOwnerReference, + hasName("postgres-myapp-database-pdb"), + hasMinAvailable(1), + testLabelsAndSelectors, }, }, - // With zero instances. { - New( + scenario: "With zero instances", + spec: New( Config{OpConfig: config.Config{Resources: config.Resources{ClusterNameLabel: "cluster-name", PodRoleLabel: "spilo-role"}, PDBNameFormat: "postgres-{cluster}-pdb"}}, k8sutil.KubernetesClient{}, acidv1.Postgresql{ @@ -2358,23 +2413,16 @@ func TestGeneratePodDisruptionBudget(t *testing.T) { Spec: acidv1.PostgresSpec{TeamID: "myapp", NumberOfInstances: 0}}, logger, eventRecorder), - policyv1.PodDisruptionBudget{ - ObjectMeta: metav1.ObjectMeta{ - Name: "postgres-myapp-database-pdb", - Namespace: "myapp", - Labels: map[string]string{"team": "myapp", "cluster-name": "myapp-database"}, - }, - Spec: policyv1.PodDisruptionBudgetSpec{ - MinAvailable: util.ToIntStr(0), - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"spilo-role": "master", "cluster-name": "myapp-database"}, - }, - }, + check: []func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error{ + testPodDisruptionBudgetOwnerReference, + hasName("postgres-myapp-database-pdb"), + hasMinAvailable(0), + testLabelsAndSelectors, }, }, - // With PodDisruptionBudget disabled. { - New( + scenario: "With PodDisruptionBudget disabled", + spec: New( Config{OpConfig: config.Config{Resources: config.Resources{ClusterNameLabel: "cluster-name", PodRoleLabel: "spilo-role"}, PDBNameFormat: "postgres-{cluster}-pdb", EnablePodDisruptionBudget: util.False()}}, k8sutil.KubernetesClient{}, acidv1.Postgresql{ @@ -2382,23 +2430,16 @@ func TestGeneratePodDisruptionBudget(t *testing.T) { Spec: acidv1.PostgresSpec{TeamID: "myapp", NumberOfInstances: 3}}, logger, eventRecorder), - policyv1.PodDisruptionBudget{ - ObjectMeta: metav1.ObjectMeta{ - Name: "postgres-myapp-database-pdb", - Namespace: "myapp", - Labels: map[string]string{"team": "myapp", "cluster-name": "myapp-database"}, - }, - Spec: policyv1.PodDisruptionBudgetSpec{ - MinAvailable: util.ToIntStr(0), - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"spilo-role": "master", "cluster-name": "myapp-database"}, - }, - }, + check: []func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error{ + testPodDisruptionBudgetOwnerReference, + hasName("postgres-myapp-database-pdb"), + hasMinAvailable(0), + testLabelsAndSelectors, }, }, - // With non-default PDBNameFormat and PodDisruptionBudget explicitly enabled. { - New( + scenario: "With non-default PDBNameFormat and PodDisruptionBudget explicitly enabled", + spec: New( Config{OpConfig: config.Config{Resources: config.Resources{ClusterNameLabel: "cluster-name", PodRoleLabel: "spilo-role"}, PDBNameFormat: "postgres-{cluster}-databass-budget", EnablePodDisruptionBudget: util.True()}}, k8sutil.KubernetesClient{}, acidv1.Postgresql{ @@ -2406,50 +2447,57 @@ func TestGeneratePodDisruptionBudget(t *testing.T) { Spec: acidv1.PostgresSpec{TeamID: "myapp", NumberOfInstances: 3}}, logger, eventRecorder), - policyv1.PodDisruptionBudget{ - ObjectMeta: metav1.ObjectMeta{ - Name: "postgres-myapp-database-databass-budget", - Namespace: "myapp", - Labels: map[string]string{"team": "myapp", "cluster-name": "myapp-database"}, - }, - Spec: policyv1.PodDisruptionBudgetSpec{ - MinAvailable: util.ToIntStr(1), - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"spilo-role": "master", "cluster-name": "myapp-database"}, - }, - }, + check: []func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error{ + testPodDisruptionBudgetOwnerReference, + hasName("postgres-myapp-database-databass-budget"), + hasMinAvailable(1), + testLabelsAndSelectors, }, }, - // With PDBMasterLabelSelector disabled. { - New( - Config{OpConfig: config.Config{Resources: config.Resources{ClusterNameLabel: "cluster-name", PodRoleLabel: "spilo-role"}, PDBNameFormat: "postgres-{cluster}-pdb", PDBMasterLabelSelector: util.False()}}, + scenario: "With PDBMasterLabelSelector disabled", + spec: New( + Config{OpConfig: config.Config{Resources: config.Resources{ClusterNameLabel: "cluster-name", PodRoleLabel: "spilo-role"}, PDBNameFormat: "postgres-{cluster}-pdb", EnablePodDisruptionBudget: util.True(), PDBMasterLabelSelector: util.False()}}, k8sutil.KubernetesClient{}, acidv1.Postgresql{ ObjectMeta: metav1.ObjectMeta{Name: "myapp-database", Namespace: "myapp"}, Spec: acidv1.PostgresSpec{TeamID: "myapp", NumberOfInstances: 3}}, logger, eventRecorder), - policyv1.PodDisruptionBudget{ - ObjectMeta: metav1.ObjectMeta{ - Name: "postgres-myapp-database-pdb", - Namespace: "myapp", - Labels: map[string]string{"team": "myapp", "cluster-name": "myapp-database"}, - }, - Spec: policyv1.PodDisruptionBudgetSpec{ - MinAvailable: util.ToIntStr(1), - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"cluster-name": "myapp-database"}, - }, - }, + check: []func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error{ + testPodDisruptionBudgetOwnerReference, + hasName("postgres-myapp-database-pdb"), + hasMinAvailable(1), + testLabelsAndSelectors, + }, + }, + { + scenario: "With OwnerReference enabled", + spec: New( + Config{OpConfig: config.Config{Resources: config.Resources{ClusterNameLabel: "cluster-name", PodRoleLabel: "spilo-role", EnableOwnerReferences: util.True()}, PDBNameFormat: "postgres-{cluster}-pdb", EnablePodDisruptionBudget: util.True()}}, + k8sutil.KubernetesClient{}, + acidv1.Postgresql{ + ObjectMeta: metav1.ObjectMeta{Name: "myapp-database", Namespace: "myapp"}, + Spec: acidv1.PostgresSpec{TeamID: "myapp", NumberOfInstances: 3}}, + logger, + eventRecorder), + check: []func(cluster *Cluster, podDisruptionBudget *policyv1.PodDisruptionBudget) error{ + testPodDisruptionBudgetOwnerReference, + hasName("postgres-myapp-database-pdb"), + hasMinAvailable(1), + testLabelsAndSelectors, }, }, } for _, tt := range tests { - result := tt.c.generatePodDisruptionBudget() - if !reflect.DeepEqual(*result, tt.out) { - t.Errorf("Expected PodDisruptionBudget: %#v, got %#v", tt.out, *result) + result := tt.spec.generatePodDisruptionBudget() + for _, check := range tt.check { + err := check(tt.spec, result) + if err != nil { + t.Errorf("%s [%s]: PodDisruptionBudget spec is incorrect, %+v", + testName, tt.scenario, err) + } } } } @@ -3541,6 +3589,11 @@ func TestGenerateLogicalBackupJob(t *testing.T) { cluster.Spec.LogicalBackupSchedule = tt.specSchedule cronJob, err := cluster.generateLogicalBackupJob() assert.NoError(t, err) + + if !reflect.DeepEqual(cronJob.ObjectMeta.OwnerReferences, cluster.ownerReferences()) { + t.Errorf("%s - %s: expected owner references %#v, got %#v", t.Name(), tt.subTest, cluster.ownerReferences(), cronJob.ObjectMeta.OwnerReferences) + } + if cronJob.Spec.Schedule != tt.expectedSchedule { t.Errorf("%s - %s: expected schedule %s, got %s", t.Name(), tt.subTest, tt.expectedSchedule, cronJob.Spec.Schedule) } diff --git a/pkg/cluster/majorversionupgrade.go b/pkg/cluster/majorversionupgrade.go index 5a1599cda..6bf4f167b 100644 --- a/pkg/cluster/majorversionupgrade.go +++ b/pkg/cluster/majorversionupgrade.go @@ -11,7 +11,6 @@ import ( // VersionMap Map of version numbers var VersionMap = map[string]int{ - "11": 110000, "12": 120000, "13": 130000, "14": 140000, @@ -74,6 +73,11 @@ func (c *Cluster) majorVersionUpgrade() error { return nil } + if !isInMainternanceWindow(c.Spec.MaintenanceWindows) { + c.logger.Infof("skipping major version upgrade, not in maintenance window") + return nil + } + pods, err := c.listPods() if err != nil { return err diff --git a/pkg/cluster/resources.go b/pkg/cluster/resources.go index 1d4758c02..f67498b61 100644 --- a/pkg/cluster/resources.go +++ b/pkg/cluster/resources.go @@ -31,20 +31,36 @@ func (c *Cluster) listResources() error { c.logger.Infof("found statefulset: %q (uid: %q)", util.NameFromMeta(c.Statefulset.ObjectMeta), c.Statefulset.UID) } - for _, obj := range c.Secrets { - c.logger.Infof("found secret: %q (uid: %q) namesapce: %s", util.NameFromMeta(obj.ObjectMeta), obj.UID, obj.ObjectMeta.Namespace) + for appId, stream := range c.Streams { + c.logger.Infof("found stream: %q with application id %q (uid: %q)", util.NameFromMeta(stream.ObjectMeta), appId, stream.UID) } - if !c.patroniKubernetesUseConfigMaps() { - for role, endpoint := range c.Endpoints { - c.logger.Infof("found %s endpoint: %q (uid: %q)", role, util.NameFromMeta(endpoint.ObjectMeta), endpoint.UID) - } + if c.LogicalBackupJob != nil { + c.logger.Infof("found logical backup job: %q (uid: %q)", util.NameFromMeta(c.LogicalBackupJob.ObjectMeta), c.LogicalBackupJob.UID) + } + + for _, secret := range c.Secrets { + c.logger.Infof("found secret: %q (uid: %q) namespace: %s", util.NameFromMeta(secret.ObjectMeta), secret.UID, secret.ObjectMeta.Namespace) } for role, service := range c.Services { c.logger.Infof("found %s service: %q (uid: %q)", role, util.NameFromMeta(service.ObjectMeta), service.UID) } + for role, endpoint := range c.Endpoints { + c.logger.Infof("found %s endpoint: %q (uid: %q)", role, util.NameFromMeta(endpoint.ObjectMeta), endpoint.UID) + } + + if c.patroniKubernetesUseConfigMaps() { + for suffix, configmap := range c.PatroniConfigMaps { + c.logger.Infof("found %s Patroni config map: %q (uid: %q)", suffix, util.NameFromMeta(configmap.ObjectMeta), configmap.UID) + } + } else { + for suffix, endpoint := range c.PatroniEndpoints { + c.logger.Infof("found %s Patroni endpoint: %q (uid: %q)", suffix, util.NameFromMeta(endpoint.ObjectMeta), endpoint.UID) + } + } + pods, err := c.listPods() if err != nil { return fmt.Errorf("could not get the list of pods: %v", err) @@ -63,6 +79,15 @@ func (c *Cluster) listResources() error { c.logger.Infof("found PVC: %q (uid: %q)", util.NameFromMeta(obj.ObjectMeta), obj.UID) } + for role, poolerObjs := range c.ConnectionPooler { + if poolerObjs.Deployment != nil { + c.logger.Infof("found %s pooler deployment: %q (uid: %q) ", role, util.NameFromMeta(poolerObjs.Deployment.ObjectMeta), poolerObjs.Deployment.UID) + } + if poolerObjs.Service != nil { + c.logger.Infof("found %s pooler service: %q (uid: %q) ", role, util.NameFromMeta(poolerObjs.Service.ObjectMeta), poolerObjs.Service.UID) + } + } + return nil } @@ -286,55 +311,37 @@ func (c *Cluster) createService(role PostgresRole) (*v1.Service, error) { } func (c *Cluster) updateService(role PostgresRole, oldService *v1.Service, newService *v1.Service) (*v1.Service, error) { - var ( - svc *v1.Service - err error - ) - - c.setProcessName("updating %v service", role) + var err error + svc := oldService serviceName := util.NameFromMeta(oldService.ObjectMeta) + match, reason := c.compareServices(oldService, newService) + if !match { + c.logServiceChanges(role, oldService, newService, false, reason) + c.setProcessName("updating %v service", role) - // update the service annotation in order to propagate ELB notation. - if len(newService.ObjectMeta.Annotations) > 0 { - if annotationsPatchData, err := metaAnnotationsPatch(newService.ObjectMeta.Annotations); err == nil { - _, err = c.KubeClient.Services(serviceName.Namespace).Patch( - context.TODO(), - serviceName.Name, - types.MergePatchType, - []byte(annotationsPatchData), - metav1.PatchOptions{}, - "") - - if err != nil { - return nil, fmt.Errorf("could not replace annotations for the service %q: %v", serviceName, err) - } - } else { - return nil, fmt.Errorf("could not form patch for the service metadata: %v", err) + // now, patch the service spec, but when disabling LoadBalancers do update instead + // patch does not work because of LoadBalancerSourceRanges field (even if set to nil) + oldServiceType := oldService.Spec.Type + newServiceType := newService.Spec.Type + if newServiceType == "ClusterIP" && newServiceType != oldServiceType { + newService.ResourceVersion = oldService.ResourceVersion + newService.Spec.ClusterIP = oldService.Spec.ClusterIP } - } - - // now, patch the service spec, but when disabling LoadBalancers do update instead - // patch does not work because of LoadBalancerSourceRanges field (even if set to nil) - oldServiceType := oldService.Spec.Type - newServiceType := newService.Spec.Type - if newServiceType == "ClusterIP" && newServiceType != oldServiceType { - newService.ResourceVersion = oldService.ResourceVersion - newService.Spec.ClusterIP = oldService.Spec.ClusterIP svc, err = c.KubeClient.Services(serviceName.Namespace).Update(context.TODO(), newService, metav1.UpdateOptions{}) if err != nil { return nil, fmt.Errorf("could not update service %q: %v", serviceName, err) } - } else { - patchData, err := specPatch(newService.Spec) - if err != nil { - return nil, fmt.Errorf("could not form patch for the service %q: %v", serviceName, err) - } + } - svc, err = c.KubeClient.Services(serviceName.Namespace).Patch( - context.TODO(), serviceName.Name, types.MergePatchType, patchData, metav1.PatchOptions{}, "") + if changed, _ := c.compareAnnotations(oldService.Annotations, newService.Annotations); changed { + patchData, err := metaAnnotationsPatch(newService.Annotations) if err != nil { - return nil, fmt.Errorf("could not patch service %q: %v", serviceName, err) + return nil, fmt.Errorf("could not form patch for service %q annotations: %v", oldService.Name, err) + } + svc, err = c.KubeClient.Services(serviceName.Namespace).Patch(context.TODO(), newService.Name, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}) + if err != nil { + return nil, fmt.Errorf("could not patch annotations for service %q: %v", oldService.Name, err) } } @@ -350,11 +357,10 @@ func (c *Cluster) deleteService(role PostgresRole) error { } if err := c.KubeClient.Services(c.Services[role].Namespace).Delete(context.TODO(), c.Services[role].Name, c.deleteOptions); err != nil { - if k8sutil.ResourceNotFound(err) { - c.logger.Debugf("%s service has already been deleted", role) - } else if err != nil { - return err + if !k8sutil.ResourceNotFound(err) { + return fmt.Errorf("could not delete %s service: %v", role, err) } + c.logger.Debugf("%s service has already been deleted", role) } c.logger.Infof("%s service %q has been deleted", role, util.NameFromMeta(c.Services[role].ObjectMeta)) @@ -496,11 +502,10 @@ func (c *Cluster) deleteEndpoint(role PostgresRole) error { } if err := c.KubeClient.Endpoints(c.Endpoints[role].Namespace).Delete(context.TODO(), c.Endpoints[role].Name, c.deleteOptions); err != nil { - if k8sutil.ResourceNotFound(err) { - c.logger.Debugf("%s endpoint has already been deleted", role) - } else if err != nil { - return fmt.Errorf("could not delete endpoint: %v", err) + if !k8sutil.ResourceNotFound(err) { + return fmt.Errorf("could not delete %s endpoint: %v", role, err) } + c.logger.Debugf("%s endpoint has already been deleted", role) } c.logger.Infof("%s endpoint %q has been deleted", role, util.NameFromMeta(c.Endpoints[role].ObjectMeta)) @@ -509,12 +514,83 @@ func (c *Cluster) deleteEndpoint(role PostgresRole) error { return nil } +func (c *Cluster) deletePatroniResources() error { + c.setProcessName("deleting Patroni resources") + errors := make([]string, 0) + + if err := c.deleteService(Patroni); err != nil { + errors = append(errors, fmt.Sprintf("%v", err)) + } + + for _, suffix := range patroniObjectSuffixes { + if c.patroniKubernetesUseConfigMaps() { + if err := c.deletePatroniConfigMap(suffix); err != nil { + errors = append(errors, fmt.Sprintf("%v", err)) + } + } else { + if err := c.deletePatroniEndpoint(suffix); err != nil { + errors = append(errors, fmt.Sprintf("%v", err)) + } + } + } + + if len(errors) > 0 { + return fmt.Errorf("%v", strings.Join(errors, `', '`)) + } + + return nil +} + +func (c *Cluster) deletePatroniConfigMap(suffix string) error { + c.setProcessName("deleting Patroni config map") + c.logger.Debugln("deleting Patroni config map") + cm := c.PatroniConfigMaps[suffix] + if cm == nil { + c.logger.Debugf("there is no %s Patroni config map in the cluster", suffix) + return nil + } + + if err := c.KubeClient.ConfigMaps(cm.Namespace).Delete(context.TODO(), cm.Name, c.deleteOptions); err != nil { + if !k8sutil.ResourceNotFound(err) { + return fmt.Errorf("could not delete %s Patroni config map %q: %v", suffix, cm.Name, err) + } + c.logger.Debugf("%s Patroni config map has already been deleted", suffix) + } + + c.logger.Infof("%s Patroni config map %q has been deleted", suffix, util.NameFromMeta(cm.ObjectMeta)) + delete(c.PatroniConfigMaps, suffix) + + return nil +} + +func (c *Cluster) deletePatroniEndpoint(suffix string) error { + c.setProcessName("deleting Patroni endpoint") + c.logger.Debugln("deleting Patroni endpoint") + ep := c.PatroniEndpoints[suffix] + if ep == nil { + c.logger.Debugf("there is no %s Patroni endpoint in the cluster", suffix) + return nil + } + + if err := c.KubeClient.Endpoints(ep.Namespace).Delete(context.TODO(), ep.Name, c.deleteOptions); err != nil { + if !k8sutil.ResourceNotFound(err) { + return fmt.Errorf("could not delete %s Patroni endpoint %q: %v", suffix, ep.Name, err) + } + c.logger.Debugf("%s Patroni endpoint has already been deleted", suffix) + } + + c.logger.Infof("%s Patroni endpoint %q has been deleted", suffix, util.NameFromMeta(ep.ObjectMeta)) + delete(c.PatroniEndpoints, suffix) + + return nil +} + func (c *Cluster) deleteSecrets() error { c.setProcessName("deleting secrets") errors := make([]string, 0) - for uid, secret := range c.Secrets { - err := c.deleteSecret(uid, *secret) + for uid := range c.Secrets { + err := c.deleteSecret(uid) if err != nil { errors = append(errors, fmt.Sprintf("%v", err)) } @@ -527,8 +603,9 @@ func (c *Cluster) deleteSecrets() error { return nil } -func (c *Cluster) deleteSecret(uid types.UID, secret v1.Secret) error { +func (c *Cluster) deleteSecret(uid types.UID) error { c.setProcessName("deleting secret") + secret := c.Secrets[uid] secretName := util.NameFromMeta(secret.ObjectMeta) c.logger.Debugf("deleting secret %q", secretName) err := c.KubeClient.Secrets(secret.Namespace).Delete(context.TODO(), secret.Name, c.deleteOptions) @@ -556,12 +633,12 @@ func (c *Cluster) createLogicalBackupJob() (err error) { if err != nil { return fmt.Errorf("could not generate k8s cron job spec: %v", err) } - c.logger.Debugf("Generated cronJobSpec: %v", logicalBackupJobSpec) - _, err = c.KubeClient.CronJobsGetter.CronJobs(c.Namespace).Create(context.TODO(), logicalBackupJobSpec, metav1.CreateOptions{}) + cronJob, err := c.KubeClient.CronJobsGetter.CronJobs(c.Namespace).Create(context.TODO(), logicalBackupJobSpec, metav1.CreateOptions{}) if err != nil { return fmt.Errorf("could not create k8s cron job: %v", err) } + c.LogicalBackupJob = cronJob return nil } @@ -575,7 +652,7 @@ func (c *Cluster) patchLogicalBackupJob(newJob *batchv1.CronJob) error { } // update the backup job spec - _, err = c.KubeClient.CronJobsGetter.CronJobs(c.Namespace).Patch( + cronJob, err := c.KubeClient.CronJobsGetter.CronJobs(c.Namespace).Patch( context.TODO(), c.getLogicalBackupJobName(), types.MergePatchType, @@ -585,20 +662,24 @@ func (c *Cluster) patchLogicalBackupJob(newJob *batchv1.CronJob) error { if err != nil { return fmt.Errorf("could not patch logical backup job: %v", err) } + c.LogicalBackupJob = cronJob return nil } func (c *Cluster) deleteLogicalBackupJob() error { - + if c.LogicalBackupJob == nil { + return nil + } c.logger.Info("removing the logical backup job") - err := c.KubeClient.CronJobsGetter.CronJobs(c.Namespace).Delete(context.TODO(), c.getLogicalBackupJobName(), c.deleteOptions) + err := c.KubeClient.CronJobsGetter.CronJobs(c.LogicalBackupJob.Namespace).Delete(context.TODO(), c.getLogicalBackupJobName(), c.deleteOptions) if k8sutil.ResourceNotFound(err) { c.logger.Debugf("logical backup cron job %q has already been deleted", c.getLogicalBackupJobName()) } else if err != nil { return err } + c.LogicalBackupJob = nil return nil } diff --git a/pkg/cluster/streams.go b/pkg/cluster/streams.go index a135c5767..9a31edc28 100644 --- a/pkg/cluster/streams.go +++ b/pkg/cluster/streams.go @@ -29,41 +29,46 @@ func (c *Cluster) createStreams(appId string) (*zalandov1.FabricEventStream, err return streamCRD, nil } -func (c *Cluster) updateStreams(newEventStreams *zalandov1.FabricEventStream) error { +func (c *Cluster) updateStreams(newEventStreams *zalandov1.FabricEventStream) (patchedStream *zalandov1.FabricEventStream, err error) { c.setProcessName("updating event streams") + patch, err := json.Marshal(newEventStreams) if err != nil { - return fmt.Errorf("could not marshal new event stream CRD %q: %v", newEventStreams.Name, err) + return nil, fmt.Errorf("could not marshal new event stream CRD %q: %v", newEventStreams.Name, err) } - if _, err := c.KubeClient.FabricEventStreams(newEventStreams.Namespace).Patch( + if patchedStream, err = c.KubeClient.FabricEventStreams(newEventStreams.Namespace).Patch( context.TODO(), newEventStreams.Name, types.MergePatchType, patch, metav1.PatchOptions{}); err != nil { - return err + return nil, err } + return patchedStream, nil +} + +func (c *Cluster) deleteStream(appId string) error { + c.setProcessName("deleting event stream") + + err := c.KubeClient.FabricEventStreams(c.Streams[appId].Namespace).Delete(context.TODO(), c.Streams[appId].Name, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("could not delete event stream %q with applicationId %s: %v", c.Streams[appId].Name, appId, err) + } + delete(c.Streams, appId) + return nil } func (c *Cluster) deleteStreams() error { - c.setProcessName("deleting event streams") - // check if stream CRD is installed before trying a delete _, err := c.KubeClient.CustomResourceDefinitions().Get(context.TODO(), constants.EventStreamCRDName, metav1.GetOptions{}) if k8sutil.ResourceNotFound(err) { return nil } - + c.setProcessName("deleting event streams") errors := make([]string, 0) - listOptions := metav1.ListOptions{ - LabelSelector: c.labelsSet(true).String(), - } - streams, err := c.KubeClient.FabricEventStreams(c.Namespace).List(context.TODO(), listOptions) - if err != nil { - return fmt.Errorf("could not list of FabricEventStreams: %v", err) - } - for _, stream := range streams.Items { - err = c.KubeClient.FabricEventStreams(stream.Namespace).Delete(context.TODO(), stream.Name, metav1.DeleteOptions{}) + + for appId := range c.Streams { + err := c.deleteStream(appId) if err != nil { - errors = append(errors, fmt.Sprintf("could not delete event stream %q: %v", stream.Name, err)) + errors = append(errors, fmt.Sprintf("%v", err)) } } @@ -74,7 +79,7 @@ func (c *Cluster) deleteStreams() error { return nil } -func gatherApplicationIds(streams []acidv1.Stream) []string { +func getDistinctApplicationIds(streams []acidv1.Stream) []string { appIds := make([]string, 0) for _, stream := range streams { if !util.SliceContains(appIds, stream.ApplicationId) { @@ -85,9 +90,10 @@ func gatherApplicationIds(streams []acidv1.Stream) []string { return appIds } -func (c *Cluster) syncPublication(publication, dbName string, tables map[string]acidv1.StreamTable) error { +func (c *Cluster) syncPublication(dbName string, databaseSlotsList map[string]zalandov1.Slot, slotsToSync *map[string]map[string]string) error { createPublications := make(map[string]string) alterPublications := make(map[string]string) + deletePublications := []string{} defer func() { if err := c.closeDbConn(); err != nil { @@ -97,7 +103,7 @@ func (c *Cluster) syncPublication(publication, dbName string, tables map[string] // check for existing publications if err := c.initDbConnWithName(dbName); err != nil { - return fmt.Errorf("could not init database connection") + return fmt.Errorf("could not init database connection: %v", err) } currentPublications, err := c.getPublications() @@ -105,36 +111,64 @@ func (c *Cluster) syncPublication(publication, dbName string, tables map[string] return fmt.Errorf("could not get current publications: %v", err) } - tableNames := make([]string, len(tables)) - i := 0 - for t := range tables { - tableName, schemaName := getTableSchema(t) - tableNames[i] = fmt.Sprintf("%s.%s", schemaName, tableName) - i++ - } - sort.Strings(tableNames) - tableList := strings.Join(tableNames, ", ") + for slotName, slotAndPublication := range databaseSlotsList { + tables := slotAndPublication.Publication + tableNames := make([]string, len(tables)) + i := 0 + for t := range tables { + tableName, schemaName := getTableSchema(t) + tableNames[i] = fmt.Sprintf("%s.%s", schemaName, tableName) + i++ + } + sort.Strings(tableNames) + tableList := strings.Join(tableNames, ", ") - currentTables, exists := currentPublications[publication] - if !exists { - createPublications[publication] = tableList - } else if currentTables != tableList { - alterPublications[publication] = tableList + currentTables, exists := currentPublications[slotName] + if !exists { + createPublications[slotName] = tableList + } else if currentTables != tableList { + alterPublications[slotName] = tableList + } else { + (*slotsToSync)[slotName] = slotAndPublication.Slot + } } - if len(createPublications)+len(alterPublications) == 0 { + // check if there is any deletion + for slotName := range currentPublications { + if _, exists := databaseSlotsList[slotName]; !exists { + deletePublications = append(deletePublications, slotName) + } + } + + if len(createPublications)+len(alterPublications)+len(deletePublications) == 0 { return nil } + errors := make([]string, 0) for publicationName, tables := range createPublications { if err = c.executeCreatePublication(publicationName, tables); err != nil { - return fmt.Errorf("creation of publication %q failed: %v", publicationName, err) + errors = append(errors, fmt.Sprintf("creation of publication %q failed: %v", publicationName, err)) + continue } + (*slotsToSync)[publicationName] = databaseSlotsList[publicationName].Slot } for publicationName, tables := range alterPublications { if err = c.executeAlterPublication(publicationName, tables); err != nil { - return fmt.Errorf("update of publication %q failed: %v", publicationName, err) + errors = append(errors, fmt.Sprintf("update of publication %q failed: %v", publicationName, err)) + continue } + (*slotsToSync)[publicationName] = databaseSlotsList[publicationName].Slot + } + for _, publicationName := range deletePublications { + if err = c.executeDropPublication(publicationName); err != nil { + errors = append(errors, fmt.Sprintf("deletion of publication %q failed: %v", publicationName, err)) + continue + } + (*slotsToSync)[publicationName] = nil + } + + if len(errors) > 0 { + return fmt.Errorf("%v", strings.Join(errors, `', '`)) } return nil @@ -168,11 +202,10 @@ func (c *Cluster) generateFabricEventStream(appId string) *zalandov1.FabricEvent }, ObjectMeta: metav1.ObjectMeta{ // max length for cluster name is 58 so we can only add 5 more characters / numbers - Name: fmt.Sprintf("%s-%s", c.Name, strings.ToLower(util.RandomPassword(5))), - Namespace: c.Namespace, - Labels: c.labelsSet(true), - Annotations: c.AnnotationsToPropagate(c.annotationsSet(nil)), - // make cluster StatefulSet the owner (like with connection pooler objects) + Name: fmt.Sprintf("%s-%s", c.Name, strings.ToLower(util.RandomPassword(5))), + Namespace: c.Namespace, + Labels: c.labelsSet(true), + Annotations: c.AnnotationsToPropagate(c.annotationsSet(nil)), OwnerReferences: c.ownerReferences(), }, Spec: zalandov1.FabricEventStreamSpec{ @@ -279,55 +312,73 @@ func (c *Cluster) syncStreams() error { return nil } - slots := make(map[string]map[string]string) + databaseSlots := make(map[string]map[string]zalandov1.Slot) slotsToSync := make(map[string]map[string]string) - publications := make(map[string]map[string]acidv1.StreamTable) requiredPatroniConfig := c.Spec.Patroni if len(requiredPatroniConfig.Slots) > 0 { - slots = requiredPatroniConfig.Slots + for slotName, slotConfig := range requiredPatroniConfig.Slots { + slotsToSync[slotName] = slotConfig + } } - // gather list of required slots and publications + if err := c.initDbConn(); err != nil { + return fmt.Errorf("could not init database connection") + } + defer func() { + if err := c.closeDbConn(); err != nil { + c.logger.Errorf("could not close database connection: %v", err) + } + }() + listDatabases, err := c.getDatabases() + if err != nil { + return fmt.Errorf("could not get list of databases: %v", err) + } + // get database name with empty list of slot, except template0 and template1 + for dbName := range listDatabases { + if dbName != "template0" && dbName != "template1" { + databaseSlots[dbName] = map[string]zalandov1.Slot{} + } + } + + // get list of required slots and publications, group by database for _, stream := range c.Spec.Streams { + if _, exists := databaseSlots[stream.Database]; !exists { + c.logger.Warningf("database %q does not exist in the cluster", stream.Database) + continue + } slot := map[string]string{ "database": stream.Database, "plugin": constants.EventStreamSourcePluginType, "type": "logical", } slotName := getSlotName(stream.Database, stream.ApplicationId) - if _, exists := slots[slotName]; !exists { - slots[slotName] = slot - publications[slotName] = stream.Tables + if _, exists := databaseSlots[stream.Database][slotName]; !exists { + databaseSlots[stream.Database][slotName] = zalandov1.Slot{ + Slot: slot, + Publication: stream.Tables, + } } else { - streamTables := publications[slotName] + slotAndPublication := databaseSlots[stream.Database][slotName] + streamTables := slotAndPublication.Publication for tableName, table := range stream.Tables { if _, exists := streamTables[tableName]; !exists { streamTables[tableName] = table } } - publications[slotName] = streamTables + slotAndPublication.Publication = streamTables + databaseSlots[stream.Database][slotName] = slotAndPublication } } - // create publications to each created slot + // sync publication in a database c.logger.Debug("syncing database publications") - for publication, tables := range publications { - // but first check for existing publications - dbName := slots[publication]["database"] - err = c.syncPublication(publication, dbName, tables) + for dbName, databaseSlotsList := range databaseSlots { + err := c.syncPublication(dbName, databaseSlotsList, &slotsToSync) if err != nil { - c.logger.Warningf("could not sync publication %q in database %q: %v", publication, dbName, err) + c.logger.Warningf("could not sync all publications in database %q: %v", dbName, err) continue } - slotsToSync[publication] = slots[publication] - } - - // no slots to sync = no streams defined or publications created - if len(slotsToSync) > 0 { - requiredPatroniConfig.Slots = slotsToSync - } else { - return nil } c.logger.Debug("syncing logical replication slots") @@ -337,70 +388,135 @@ func (c *Cluster) syncStreams() error { } // sync logical replication slots in Patroni config + requiredPatroniConfig.Slots = slotsToSync configPatched, _, _, err := c.syncPatroniConfig(pods, requiredPatroniConfig, nil) if err != nil { c.logger.Warningf("Patroni config updated? %v - errors during config sync: %v", configPatched, err) } // finally sync stream CRDs - err = c.createOrUpdateStreams() - if err != nil { - return err - } - - return nil -} - -func (c *Cluster) createOrUpdateStreams() error { - - // fetch different application IDs from streams section + // get distinct application IDs from streams section // there will be a separate event stream resource for each ID - appIds := gatherApplicationIds(c.Spec.Streams) - - // list all existing stream CRDs - listOptions := metav1.ListOptions{ - LabelSelector: c.labelsSet(true).String(), - } - streams, err := c.KubeClient.FabricEventStreams(c.Namespace).List(context.TODO(), listOptions) - if err != nil { - return fmt.Errorf("could not list of FabricEventStreams: %v", err) - } - + appIds := getDistinctApplicationIds(c.Spec.Streams) for _, appId := range appIds { - streamExists := false - - // update stream when it exists and EventStreams array differs - for _, stream := range streams.Items { - if appId == stream.Spec.ApplicationId { - streamExists = true - desiredStreams := c.generateFabricEventStream(appId) - if match, reason := sameStreams(stream.Spec.EventStreams, desiredStreams.Spec.EventStreams); !match { - c.logger.Debugf("updating event streams: %s", reason) - desiredStreams.ObjectMeta = stream.ObjectMeta - err = c.updateStreams(desiredStreams) - if err != nil { - return fmt.Errorf("failed updating event stream %s: %v", stream.Name, err) - } - c.logger.Infof("event stream %q has been successfully updated", stream.Name) - } - continue + if hasSlotsInSync(appId, databaseSlots, slotsToSync) { + if err = c.syncStream(appId); err != nil { + c.logger.Warningf("could not sync event streams with applicationId %s: %v", appId, err) } + } else { + c.logger.Warningf("database replication slots %#v for streams with applicationId %s not in sync, skipping event stream sync", slotsToSync, appId) } + } - if !streamExists { - c.logger.Infof("event streams with applicationId %s do not exist, create it", appId) - streamCRD, err := c.createStreams(appId) - if err != nil { - return fmt.Errorf("failed creating event streams with applicationId %s: %v", appId, err) - } - c.logger.Infof("event streams %q have been successfully created", streamCRD.Name) - } + // check if there is any deletion + if err = c.cleanupRemovedStreams(appIds); err != nil { + return fmt.Errorf("%v", err) } return nil } -func sameStreams(curEventStreams, newEventStreams []zalandov1.EventStream) (match bool, reason string) { +func hasSlotsInSync(appId string, databaseSlots map[string]map[string]zalandov1.Slot, slotsToSync map[string]map[string]string) bool { + allSlotsInSync := true + for dbName, slots := range databaseSlots { + for slotName := range slots { + if slotName == getSlotName(dbName, appId) { + if slot, exists := slotsToSync[slotName]; !exists || slot == nil { + allSlotsInSync = false + continue + } + } + } + } + + return allSlotsInSync +} + +func (c *Cluster) syncStream(appId string) error { + var ( + streams *zalandov1.FabricEventStreamList + err error + ) + c.setProcessName("syncing stream with applicationId %s", appId) + c.logger.Debugf("syncing stream with applicationId %s", appId) + + listOptions := metav1.ListOptions{LabelSelector: c.labelsSet(true).String()} + streams, err = c.KubeClient.FabricEventStreams(c.Namespace).List(context.TODO(), listOptions) + if err != nil { + return fmt.Errorf("could not list of FabricEventStreams for applicationId %s: %v", appId, err) + } + + streamExists := false + for _, stream := range streams.Items { + if stream.Spec.ApplicationId != appId { + continue + } + if streamExists { + c.logger.Warningf("more than one event stream with applicationId %s found, delete it", appId) + if err = c.KubeClient.FabricEventStreams(stream.ObjectMeta.Namespace).Delete(context.TODO(), stream.ObjectMeta.Name, metav1.DeleteOptions{}); err != nil { + c.logger.Errorf("could not delete event stream %q with applicationId %s: %v", stream.ObjectMeta.Name, appId, err) + } else { + c.logger.Infof("redundant event stream %q with applicationId %s has been successfully deleted", stream.ObjectMeta.Name, appId) + } + continue + } + streamExists = true + desiredStreams := c.generateFabricEventStream(appId) + if !reflect.DeepEqual(stream.ObjectMeta.OwnerReferences, desiredStreams.ObjectMeta.OwnerReferences) { + c.logger.Infof("owner references of event streams with applicationId %s do not match the current ones", appId) + stream.ObjectMeta.OwnerReferences = desiredStreams.ObjectMeta.OwnerReferences + c.setProcessName("updating event streams with applicationId %s", appId) + stream, err := c.KubeClient.FabricEventStreams(stream.Namespace).Update(context.TODO(), &stream, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("could not update event streams with applicationId %s: %v", appId, err) + } + c.Streams[appId] = stream + } + if match, reason := c.compareStreams(&stream, desiredStreams); !match { + c.logger.Debugf("updating event streams with applicationId %s: %s", appId, reason) + desiredStreams.ObjectMeta = stream.ObjectMeta + updatedStream, err := c.updateStreams(desiredStreams) + if err != nil { + return fmt.Errorf("failed updating event streams %s with applicationId %s: %v", stream.Name, appId, err) + } + c.Streams[appId] = updatedStream + c.logger.Infof("event streams %q with applicationId %s have been successfully updated", updatedStream.Name, appId) + } + } + + if !streamExists { + c.logger.Infof("event streams with applicationId %s do not exist, create it", appId) + createdStream, err := c.createStreams(appId) + if err != nil { + return fmt.Errorf("failed creating event streams with applicationId %s: %v", appId, err) + } + c.logger.Infof("event streams %q have been successfully created", createdStream.Name) + c.Streams[appId] = createdStream + } + + return nil +} + +func (c *Cluster) compareStreams(curEventStreams, newEventStreams *zalandov1.FabricEventStream) (match bool, reason string) { + reasons := make([]string, 0) + match = true + + // stream operator can add extra annotations so incl. current annotations in desired annotations + desiredAnnotations := c.annotationsSet(curEventStreams.Annotations) + if changed, reason := c.compareAnnotations(curEventStreams.ObjectMeta.Annotations, desiredAnnotations); changed { + match = false + reasons = append(reasons, fmt.Sprintf("new streams annotations do not match: %s", reason)) + } + + if changed, reason := sameEventStreams(curEventStreams.Spec.EventStreams, newEventStreams.Spec.EventStreams); !changed { + match = false + reasons = append(reasons, fmt.Sprintf("new streams EventStreams array does not match : %s", reason)) + } + + return match, strings.Join(reasons, ", ") +} + +func sameEventStreams(curEventStreams, newEventStreams []zalandov1.EventStream) (match bool, reason string) { if len(newEventStreams) != len(curEventStreams) { return false, "number of defined streams is different" } @@ -424,3 +540,23 @@ func sameStreams(curEventStreams, newEventStreams []zalandov1.EventStream) (matc return true, "" } + +func (c *Cluster) cleanupRemovedStreams(appIds []string) error { + errors := make([]string, 0) + for appId := range c.Streams { + if !util.SliceContains(appIds, appId) { + c.logger.Infof("event streams with applicationId %s do not exist in the manifest, delete it", appId) + err := c.deleteStream(appId) + if err != nil { + errors = append(errors, fmt.Sprintf("failed deleting event streams with applicationId %s: %v", appId, err)) + } + c.logger.Infof("event streams with applicationId %s have been successfully deleted", appId) + } + } + + if len(errors) > 0 { + return fmt.Errorf("could not delete all removed event streams: %v", strings.Join(errors, `', '`)) + } + + return nil +} diff --git a/pkg/cluster/streams_test.go b/pkg/cluster/streams_test.go index f71178823..6091210b5 100644 --- a/pkg/cluster/streams_test.go +++ b/pkg/cluster/streams_test.go @@ -2,6 +2,7 @@ package cluster import ( "fmt" + "reflect" "strings" "context" @@ -18,29 +19,25 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes/fake" ) -func newFakeK8sStreamClient() (k8sutil.KubernetesClient, *fake.Clientset) { - zalandoClientSet := fakezalandov1.NewSimpleClientset() - clientSet := fake.NewSimpleClientset() - - return k8sutil.KubernetesClient{ - FabricEventStreamsGetter: zalandoClientSet.ZalandoV1(), - PostgresqlsGetter: zalandoClientSet.AcidV1(), - PodsGetter: clientSet.CoreV1(), - StatefulSetsGetter: clientSet.AppsV1(), - }, clientSet -} - var ( - clusterName string = "acid-test-cluster" + clusterName string = "acid-stream-cluster" namespace string = "default" appId string = "test-app" dbName string = "foo" fesUser string = fmt.Sprintf("%s%s", constants.EventStreamSourceSlotPrefix, constants.UserRoleNameSuffix) slotName string = fmt.Sprintf("%s_%s_%s", constants.EventStreamSourceSlotPrefix, dbName, strings.Replace(appId, "-", "_", -1)) + zalandoClientSet = fakezalandov1.NewSimpleClientset() + + client = k8sutil.KubernetesClient{ + FabricEventStreamsGetter: zalandoClientSet.ZalandoV1(), + PostgresqlsGetter: zalandoClientSet.AcidV1(), + PodsGetter: clientSet.CoreV1(), + StatefulSetsGetter: clientSet.AppsV1(), + } + pg = acidv1.Postgresql{ TypeMeta: metav1.TypeMeta{ Kind: "Postgresql", @@ -91,6 +88,11 @@ var ( ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-12345", clusterName), Namespace: namespace, + Labels: map[string]string{ + "application": "spilo", + "cluster-name": fmt.Sprintf("%s-2", clusterName), + "team": "acid", + }, OwnerReferences: []metav1.OwnerReference{ metav1.OwnerReference{ APIVersion: "apps/v1", @@ -181,21 +183,8 @@ var ( }, }, } -) -func TestGatherApplicationIds(t *testing.T) { - testAppIds := []string{appId} - appIds := gatherApplicationIds(pg.Spec.Streams) - - if !util.IsEqualIgnoreOrder(testAppIds, appIds) { - t.Errorf("gathered applicationIds do not match, expected %#v, got %#v", testAppIds, appIds) - } -} - -func TestGenerateFabricEventStream(t *testing.T) { - client, _ := newFakeK8sStreamClient() - - var cluster = New( + cluster = New( Config{ OpConfig: config.Config{ Auth: config.Auth{ @@ -213,21 +202,249 @@ func TestGenerateFabricEventStream(t *testing.T) { }, }, }, client, pg, logger, eventRecorder) +) +func TestGatherApplicationIds(t *testing.T) { + testAppIds := []string{appId} + appIds := getDistinctApplicationIds(pg.Spec.Streams) + + if !util.IsEqualIgnoreOrder(testAppIds, appIds) { + t.Errorf("list of applicationIds does not match, expected %#v, got %#v", testAppIds, appIds) + } +} + +func TestHasSlotsInSync(t *testing.T) { cluster.Name = clusterName cluster.Namespace = namespace - // create statefulset to have ownerReference for streams - _, err := cluster.createStatefulSet() - assert.NoError(t, err) + appId2 := fmt.Sprintf("%s-2", appId) + dbNotExists := "dbnotexists" + slotNotExists := fmt.Sprintf("%s_%s_%s", constants.EventStreamSourceSlotPrefix, dbNotExists, strings.Replace(appId, "-", "_", -1)) + slotNotExistsAppId2 := fmt.Sprintf("%s_%s_%s", constants.EventStreamSourceSlotPrefix, dbNotExists, strings.Replace(appId2, "-", "_", -1)) + + tests := []struct { + subTest string + applicationId string + expectedSlots map[string]map[string]zalandov1.Slot + actualSlots map[string]map[string]string + slotsInSync bool + }{ + { + subTest: fmt.Sprintf("slots in sync for applicationId %s", appId), + applicationId: appId, + expectedSlots: map[string]map[string]zalandov1.Slot{ + dbName: { + slotName: zalandov1.Slot{ + Slot: map[string]string{ + "databases": dbName, + "plugin": constants.EventStreamSourcePluginType, + "type": "logical", + }, + Publication: map[string]acidv1.StreamTable{ + "test1": acidv1.StreamTable{ + EventType: "stream-type-a", + }, + }, + }, + }, + }, + actualSlots: map[string]map[string]string{ + slotName: map[string]string{ + "databases": dbName, + "plugin": constants.EventStreamSourcePluginType, + "type": "logical", + }, + }, + slotsInSync: true, + }, { + subTest: fmt.Sprintf("slots empty for applicationId %s after create or update of publication failed", appId), + applicationId: appId, + expectedSlots: map[string]map[string]zalandov1.Slot{ + dbNotExists: { + slotNotExists: zalandov1.Slot{ + Slot: map[string]string{ + "databases": dbName, + "plugin": constants.EventStreamSourcePluginType, + "type": "logical", + }, + Publication: map[string]acidv1.StreamTable{ + "test1": acidv1.StreamTable{ + EventType: "stream-type-a", + }, + }, + }, + }, + }, + actualSlots: map[string]map[string]string{}, + slotsInSync: false, + }, { + subTest: fmt.Sprintf("slot with empty definition for applicationId %s after publication git deleted", appId), + applicationId: appId, + expectedSlots: map[string]map[string]zalandov1.Slot{ + dbNotExists: { + slotNotExists: zalandov1.Slot{ + Slot: map[string]string{ + "databases": dbName, + "plugin": constants.EventStreamSourcePluginType, + "type": "logical", + }, + Publication: map[string]acidv1.StreamTable{ + "test1": acidv1.StreamTable{ + EventType: "stream-type-a", + }, + }, + }, + }, + }, + actualSlots: map[string]map[string]string{ + slotName: nil, + }, + slotsInSync: false, + }, { + subTest: fmt.Sprintf("one slot not in sync for applicationId %s because database does not exist", appId), + applicationId: appId, + expectedSlots: map[string]map[string]zalandov1.Slot{ + dbName: { + slotName: zalandov1.Slot{ + Slot: map[string]string{ + "databases": dbName, + "plugin": constants.EventStreamSourcePluginType, + "type": "logical", + }, + Publication: map[string]acidv1.StreamTable{ + "test1": acidv1.StreamTable{ + EventType: "stream-type-a", + }, + }, + }, + }, + dbNotExists: { + slotNotExists: zalandov1.Slot{ + Slot: map[string]string{ + "databases": "dbnotexists", + "plugin": constants.EventStreamSourcePluginType, + "type": "logical", + }, + Publication: map[string]acidv1.StreamTable{ + "test2": acidv1.StreamTable{ + EventType: "stream-type-b", + }, + }, + }, + }, + }, + actualSlots: map[string]map[string]string{ + slotName: map[string]string{ + "databases": dbName, + "plugin": constants.EventStreamSourcePluginType, + "type": "logical", + }, + }, + slotsInSync: false, + }, { + subTest: fmt.Sprintf("slots in sync for applicationId %s, but not for %s - checking %s should return true", appId, appId2, appId), + applicationId: appId, + expectedSlots: map[string]map[string]zalandov1.Slot{ + dbName: { + slotName: zalandov1.Slot{ + Slot: map[string]string{ + "databases": dbName, + "plugin": constants.EventStreamSourcePluginType, + "type": "logical", + }, + Publication: map[string]acidv1.StreamTable{ + "test1": acidv1.StreamTable{ + EventType: "stream-type-a", + }, + }, + }, + }, + dbNotExists: { + slotNotExistsAppId2: zalandov1.Slot{ + Slot: map[string]string{ + "databases": "dbnotexists", + "plugin": constants.EventStreamSourcePluginType, + "type": "logical", + }, + Publication: map[string]acidv1.StreamTable{ + "test2": acidv1.StreamTable{ + EventType: "stream-type-b", + }, + }, + }, + }, + }, + actualSlots: map[string]map[string]string{ + slotName: map[string]string{ + "databases": dbName, + "plugin": constants.EventStreamSourcePluginType, + "type": "logical", + }, + }, + slotsInSync: true, + }, { + subTest: fmt.Sprintf("slots in sync for applicationId %s, but not for %s - checking %s should return false", appId, appId2, appId2), + applicationId: appId2, + expectedSlots: map[string]map[string]zalandov1.Slot{ + dbName: { + slotName: zalandov1.Slot{ + Slot: map[string]string{ + "databases": dbName, + "plugin": constants.EventStreamSourcePluginType, + "type": "logical", + }, + Publication: map[string]acidv1.StreamTable{ + "test1": acidv1.StreamTable{ + EventType: "stream-type-a", + }, + }, + }, + }, + dbNotExists: { + slotNotExistsAppId2: zalandov1.Slot{ + Slot: map[string]string{ + "databases": "dbnotexists", + "plugin": constants.EventStreamSourcePluginType, + "type": "logical", + }, + Publication: map[string]acidv1.StreamTable{ + "test2": acidv1.StreamTable{ + EventType: "stream-type-b", + }, + }, + }, + }, + }, + actualSlots: map[string]map[string]string{ + slotName: map[string]string{ + "databases": dbName, + "plugin": constants.EventStreamSourcePluginType, + "type": "logical", + }, + }, + slotsInSync: false, + }, + } + + for _, tt := range tests { + result := hasSlotsInSync(tt.applicationId, tt.expectedSlots, tt.actualSlots) + if result != tt.slotsInSync { + t.Errorf("%s: unexpected result for slot test of applicationId: %v, expected slots %#v, actual slots %#v", tt.subTest, tt.applicationId, tt.expectedSlots, tt.actualSlots) + } + } +} + +func TestGenerateFabricEventStream(t *testing.T) { + cluster.Name = clusterName + cluster.Namespace = namespace // create the streams - err = cluster.createOrUpdateStreams() + err := cluster.syncStream(appId) assert.NoError(t, err) // compare generated stream with expected stream result := cluster.generateFabricEventStream(appId) - if match, _ := sameStreams(result.Spec.EventStreams, fes.Spec.EventStreams); !match { + if match, _ := cluster.compareStreams(result, fes); !match { t.Errorf("malformed FabricEventStream, expected %#v, got %#v", fes, result) } @@ -236,37 +453,100 @@ func TestGenerateFabricEventStream(t *testing.T) { } streams, err := cluster.KubeClient.FabricEventStreams(namespace).List(context.TODO(), listOptions) assert.NoError(t, err) - - // check if there is only one stream - if len(streams.Items) > 1 { - t.Errorf("too many stream CRDs found: got %d, but expected only one", len(streams.Items)) - } + assert.Equalf(t, 1, len(streams.Items), "unexpected number of streams found: got %d, but expected only one", len(streams.Items)) // compare stream returned from API with expected stream - if match, _ := sameStreams(streams.Items[0].Spec.EventStreams, fes.Spec.EventStreams); !match { + if match, _ := cluster.compareStreams(&streams.Items[0], fes); !match { t.Errorf("malformed FabricEventStream returned from API, expected %#v, got %#v", fes, streams.Items[0]) } // sync streams once again - err = cluster.createOrUpdateStreams() + err = cluster.syncStream(appId) assert.NoError(t, err) streams, err = cluster.KubeClient.FabricEventStreams(namespace).List(context.TODO(), listOptions) assert.NoError(t, err) - - // check if there is still only one stream - if len(streams.Items) > 1 { - t.Errorf("too many stream CRDs found after sync: got %d, but expected only one", len(streams.Items)) - } + assert.Equalf(t, 1, len(streams.Items), "unexpected number of streams found: got %d, but expected only one", len(streams.Items)) // compare stream resturned from API with generated stream - if match, _ := sameStreams(streams.Items[0].Spec.EventStreams, result.Spec.EventStreams); !match { + if match, _ := cluster.compareStreams(&streams.Items[0], result); !match { t.Errorf("returned FabricEventStream differs from generated one, expected %#v, got %#v", result, streams.Items[0]) } } +func newFabricEventStream(streams []zalandov1.EventStream, annotations map[string]string) *zalandov1.FabricEventStream { + return &zalandov1.FabricEventStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-12345", clusterName), + Annotations: annotations, + }, + Spec: zalandov1.FabricEventStreamSpec{ + ApplicationId: appId, + EventStreams: streams, + }, + } +} + +func TestSyncStreams(t *testing.T) { + pg.Name = fmt.Sprintf("%s-2", pg.Name) + var cluster = New( + Config{ + OpConfig: config.Config{ + PodManagementPolicy: "ordered_ready", + Resources: config.Resources{ + ClusterLabels: map[string]string{"application": "spilo"}, + ClusterNameLabel: "cluster-name", + DefaultCPURequest: "300m", + DefaultCPULimit: "300m", + DefaultMemoryRequest: "300Mi", + DefaultMemoryLimit: "300Mi", + EnableOwnerReferences: util.True(), + PodRoleLabel: "spilo-role", + }, + }, + }, client, pg, logger, eventRecorder) + + _, err := cluster.KubeClient.Postgresqls(namespace).Create( + context.TODO(), &pg, metav1.CreateOptions{}) + assert.NoError(t, err) + + // create the stream + err = cluster.syncStream(appId) + assert.NoError(t, err) + + // create a second stream with same spec but with different name + createdStream, err := cluster.KubeClient.FabricEventStreams(namespace).Create( + context.TODO(), fes, metav1.CreateOptions{}) + assert.NoError(t, err) + assert.Equal(t, createdStream.Spec.ApplicationId, appId) + + // check that two streams exist + listOptions := metav1.ListOptions{ + LabelSelector: cluster.labelsSet(true).String(), + } + streams, err := cluster.KubeClient.FabricEventStreams(namespace).List(context.TODO(), listOptions) + assert.NoError(t, err) + assert.Equalf(t, 2, len(streams.Items), "unexpected number of streams found: got %d, but expected only 2", len(streams.Items)) + + // sync the stream which should remove the redundant stream + err = cluster.syncStream(appId) + assert.NoError(t, err) + + // check that only one stream remains after sync + streams, err = cluster.KubeClient.FabricEventStreams(namespace).List(context.TODO(), listOptions) + assert.NoError(t, err) + assert.Equalf(t, 1, len(streams.Items), "unexpected number of streams found: got %d, but expected only 1", len(streams.Items)) + + // check owner references + if !reflect.DeepEqual(streams.Items[0].OwnerReferences, cluster.ownerReferences()) { + t.Errorf("unexpected owner references, expected %#v, got %#v", cluster.ownerReferences(), streams.Items[0].OwnerReferences) + } +} + func TestSameStreams(t *testing.T) { testName := "TestSameStreams" + annotationsA := map[string]string{"owned-by": "acid"} + annotationsB := map[string]string{"owned-by": "foo"} stream1 := zalandov1.EventStream{ EventStreamFlow: zalandov1.EventStreamFlow{}, @@ -311,57 +591,64 @@ func TestSameStreams(t *testing.T) { tests := []struct { subTest string - streamsA []zalandov1.EventStream - streamsB []zalandov1.EventStream + streamsA *zalandov1.FabricEventStream + streamsB *zalandov1.FabricEventStream match bool reason string }{ { subTest: "identical streams", - streamsA: []zalandov1.EventStream{stream1, stream2}, - streamsB: []zalandov1.EventStream{stream1, stream2}, + streamsA: newFabricEventStream([]zalandov1.EventStream{stream1, stream2}, annotationsA), + streamsB: newFabricEventStream([]zalandov1.EventStream{stream1, stream2}, annotationsA), match: true, reason: "", }, { subTest: "same streams different order", - streamsA: []zalandov1.EventStream{stream1, stream2}, - streamsB: []zalandov1.EventStream{stream2, stream1}, + streamsA: newFabricEventStream([]zalandov1.EventStream{stream1, stream2}, nil), + streamsB: newFabricEventStream([]zalandov1.EventStream{stream2, stream1}, nil), match: true, reason: "", }, { subTest: "same streams different order", - streamsA: []zalandov1.EventStream{stream1}, - streamsB: []zalandov1.EventStream{stream1, stream2}, + streamsA: newFabricEventStream([]zalandov1.EventStream{stream1}, nil), + streamsB: newFabricEventStream([]zalandov1.EventStream{stream1, stream2}, nil), match: false, reason: "number of defined streams is different", }, { subTest: "different number of streams", - streamsA: []zalandov1.EventStream{stream1}, - streamsB: []zalandov1.EventStream{stream1, stream2}, + streamsA: newFabricEventStream([]zalandov1.EventStream{stream1}, nil), + streamsB: newFabricEventStream([]zalandov1.EventStream{stream1, stream2}, nil), match: false, reason: "number of defined streams is different", }, { subTest: "event stream specs differ", - streamsA: []zalandov1.EventStream{stream1, stream2}, - streamsB: fes.Spec.EventStreams, + streamsA: newFabricEventStream([]zalandov1.EventStream{stream1, stream2}, nil), + streamsB: fes, match: false, reason: "number of defined streams is different", }, { subTest: "event stream recovery specs differ", - streamsA: []zalandov1.EventStream{stream2}, - streamsB: []zalandov1.EventStream{stream3}, + streamsA: newFabricEventStream([]zalandov1.EventStream{stream2}, nil), + streamsB: newFabricEventStream([]zalandov1.EventStream{stream3}, nil), + match: false, + reason: "event stream specs differ", + }, + { + subTest: "event stream annotations differ", + streamsA: newFabricEventStream([]zalandov1.EventStream{stream2}, annotationsA), + streamsB: newFabricEventStream([]zalandov1.EventStream{stream3}, annotationsB), match: false, reason: "event stream specs differ", }, } for _, tt := range tests { - streamsMatch, matchReason := sameStreams(tt.streamsA, tt.streamsB) + streamsMatch, matchReason := cluster.compareStreams(tt.streamsA, tt.streamsB) if streamsMatch != tt.match { t.Errorf("%s %s: unexpected match result when comparing streams: got %s, epxected %s", testName, tt.subTest, matchReason, tt.reason) @@ -369,9 +656,8 @@ func TestSameStreams(t *testing.T) { } } -func TestUpdateFabricEventStream(t *testing.T) { - client, _ := newFakeK8sStreamClient() - +func TestUpdateStreams(t *testing.T) { + pg.Name = fmt.Sprintf("%s-3", pg.Name) var cluster = New( Config{ OpConfig: config.Config{ @@ -392,12 +678,8 @@ func TestUpdateFabricEventStream(t *testing.T) { context.TODO(), &pg, metav1.CreateOptions{}) assert.NoError(t, err) - // create statefulset to have ownerReference for streams - _, err = cluster.createStatefulSet() - assert.NoError(t, err) - - // now create the stream - err = cluster.createOrUpdateStreams() + // create the stream + err = cluster.syncStream(appId) assert.NoError(t, err) // change specs of streams and patch CRD @@ -411,7 +693,50 @@ func TestUpdateFabricEventStream(t *testing.T) { } } - patchData, err := specPatch(pg.Spec) + // compare stream returned from API with expected stream + listOptions := metav1.ListOptions{ + LabelSelector: cluster.labelsSet(true).String(), + } + streams := patchPostgresqlStreams(t, cluster, &pg.Spec, listOptions) + result := cluster.generateFabricEventStream(appId) + if match, _ := cluster.compareStreams(&streams.Items[0], result); !match { + t.Errorf("Malformed FabricEventStream after updating manifest, expected %#v, got %#v", streams.Items[0], result) + } + + // disable recovery + for idx, stream := range pg.Spec.Streams { + if stream.ApplicationId == appId { + stream.EnableRecovery = util.False() + pg.Spec.Streams[idx] = stream + } + } + + streams = patchPostgresqlStreams(t, cluster, &pg.Spec, listOptions) + result = cluster.generateFabricEventStream(appId) + if match, _ := cluster.compareStreams(&streams.Items[0], result); !match { + t.Errorf("Malformed FabricEventStream after disabling event recovery, expected %#v, got %#v", streams.Items[0], result) + } + + mockClient := k8sutil.NewMockKubernetesClient() + cluster.KubeClient.CustomResourceDefinitionsGetter = mockClient.CustomResourceDefinitionsGetter + + // remove streams from manifest + pg.Spec.Streams = nil + pgUpdated, err := cluster.KubeClient.Postgresqls(namespace).Update( + context.TODO(), &pg, metav1.UpdateOptions{}) + assert.NoError(t, err) + + appIds := getDistinctApplicationIds(pgUpdated.Spec.Streams) + cluster.cleanupRemovedStreams(appIds) + + streams, err = cluster.KubeClient.FabricEventStreams(namespace).List(context.TODO(), listOptions) + if len(streams.Items) > 0 || err != nil { + t.Errorf("stream resource has not been removed or unexpected error %v", err) + } +} + +func patchPostgresqlStreams(t *testing.T, cluster *Cluster, pgSpec *acidv1.PostgresSpec, listOptions metav1.ListOptions) (streams *zalandov1.FabricEventStreamList) { + patchData, err := specPatch(pgSpec) assert.NoError(t, err) pgPatched, err := cluster.KubeClient.Postgresqls(namespace).Patch( @@ -419,40 +744,11 @@ func TestUpdateFabricEventStream(t *testing.T) { assert.NoError(t, err) cluster.Postgresql.Spec = pgPatched.Spec - err = cluster.createOrUpdateStreams() + err = cluster.syncStream(appId) assert.NoError(t, err) - // compare stream returned from API with expected stream - listOptions := metav1.ListOptions{ - LabelSelector: cluster.labelsSet(true).String(), - } - streams, err := cluster.KubeClient.FabricEventStreams(namespace).List(context.TODO(), listOptions) + streams, err = cluster.KubeClient.FabricEventStreams(namespace).List(context.TODO(), listOptions) assert.NoError(t, err) - result := cluster.generateFabricEventStream(appId) - if match, _ := sameStreams(streams.Items[0].Spec.EventStreams, result.Spec.EventStreams); !match { - t.Errorf("Malformed FabricEventStream after updating manifest, expected %#v, got %#v", streams.Items[0], result) - } - - // disable recovery - for _, stream := range pg.Spec.Streams { - if stream.ApplicationId == appId { - stream.EnableRecovery = util.False() - } - } - patchData, err = specPatch(pg.Spec) - assert.NoError(t, err) - - pgPatched, err = cluster.KubeClient.Postgresqls(namespace).Patch( - context.TODO(), cluster.Name, types.MergePatchType, patchData, metav1.PatchOptions{}, "spec") - assert.NoError(t, err) - - cluster.Postgresql.Spec = pgPatched.Spec - err = cluster.createOrUpdateStreams() - assert.NoError(t, err) - - result = cluster.generateFabricEventStream(appId) - if match, _ := sameStreams(streams.Items[0].Spec.EventStreams, result.Spec.EventStreams); !match { - t.Errorf("Malformed FabricEventStream after disabling event recovery, expected %#v, got %#v", streams.Items[0], result) - } + return streams } diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index 4c89c98fe..ee1713c05 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -15,11 +15,13 @@ import ( "github.com/zalando/postgres-operator/pkg/util" "github.com/zalando/postgres-operator/pkg/util/constants" "github.com/zalando/postgres-operator/pkg/util/k8sutil" + "golang.org/x/exp/maps" "golang.org/x/exp/slices" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" policyv1 "k8s.io/api/policy/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ) var requirePrimaryRestartWhenDecreased = []string{ @@ -79,6 +81,10 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error { return err } + if err = c.syncPatroniResources(); err != nil { + c.logger.Errorf("could not sync Patroni resources: %v", err) + } + // sync volume may already transition volumes to gp3, if iops/throughput or type is specified if err = c.syncVolumes(); err != nil { return err @@ -91,7 +97,6 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error { } } - c.logger.Debug("syncing statefulsets") if err = c.syncStatefulSet(); err != nil { if !k8sutil.ResourceAlreadyExists(err) { err = fmt.Errorf("could not sync statefulsets: %v", err) @@ -173,6 +178,166 @@ func (c *Cluster) syncFinalizer() error { return nil } +func (c *Cluster) syncPatroniResources() error { + errors := make([]string, 0) + + if err := c.syncPatroniService(); err != nil { + errors = append(errors, fmt.Sprintf("could not sync %s service: %v", Patroni, err)) + } + + for _, suffix := range patroniObjectSuffixes { + if c.patroniKubernetesUseConfigMaps() { + if err := c.syncPatroniConfigMap(suffix); err != nil { + errors = append(errors, fmt.Sprintf("could not sync %s Patroni config map: %v", suffix, err)) + } + } else { + if err := c.syncPatroniEndpoint(suffix); err != nil { + errors = append(errors, fmt.Sprintf("could not sync %s Patroni endpoint: %v", suffix, err)) + } + } + } + + if len(errors) > 0 { + return fmt.Errorf("%v", strings.Join(errors, `', '`)) + } + + return nil +} + +func (c *Cluster) syncPatroniConfigMap(suffix string) error { + var ( + cm *v1.ConfigMap + err error + ) + configMapName := fmt.Sprintf("%s-%s", c.Name, suffix) + c.logger.Debugf("syncing %s config map", configMapName) + c.setProcessName("syncing %s config map", configMapName) + + if cm, err = c.KubeClient.ConfigMaps(c.Namespace).Get(context.TODO(), configMapName, metav1.GetOptions{}); err == nil { + c.PatroniConfigMaps[suffix] = cm + desiredOwnerRefs := c.ownerReferences() + if !reflect.DeepEqual(cm.ObjectMeta.OwnerReferences, desiredOwnerRefs) { + c.logger.Infof("new %s config map's owner references do not match the current ones", configMapName) + cm.ObjectMeta.OwnerReferences = desiredOwnerRefs + c.setProcessName("updating %s config map", configMapName) + cm, err = c.KubeClient.ConfigMaps(c.Namespace).Update(context.TODO(), cm, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("could not update %s config map: %v", configMapName, err) + } + c.PatroniConfigMaps[suffix] = cm + } + annotations := make(map[string]string) + maps.Copy(annotations, cm.Annotations) + // Patroni can add extra annotations so incl. current annotations in desired annotations + desiredAnnotations := c.annotationsSet(cm.Annotations) + if changed, _ := c.compareAnnotations(annotations, desiredAnnotations); changed { + patchData, err := metaAnnotationsPatch(desiredAnnotations) + if err != nil { + return fmt.Errorf("could not form patch for %s config map: %v", configMapName, err) + } + cm, err = c.KubeClient.ConfigMaps(c.Namespace).Patch(context.TODO(), configMapName, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}) + if err != nil { + return fmt.Errorf("could not patch annotations of %s config map: %v", configMapName, err) + } + c.PatroniConfigMaps[suffix] = cm + } + } else if !k8sutil.ResourceNotFound(err) { + // if config map does not exist yet, Patroni should create it + return fmt.Errorf("could not get %s config map: %v", configMapName, err) + } + + return nil +} + +func (c *Cluster) syncPatroniEndpoint(suffix string) error { + var ( + ep *v1.Endpoints + err error + ) + endpointName := fmt.Sprintf("%s-%s", c.Name, suffix) + c.logger.Debugf("syncing %s endpoint", endpointName) + c.setProcessName("syncing %s endpoint", endpointName) + + if ep, err = c.KubeClient.Endpoints(c.Namespace).Get(context.TODO(), endpointName, metav1.GetOptions{}); err == nil { + c.PatroniEndpoints[suffix] = ep + desiredOwnerRefs := c.ownerReferences() + if !reflect.DeepEqual(ep.ObjectMeta.OwnerReferences, desiredOwnerRefs) { + c.logger.Infof("new %s endpoints's owner references do not match the current ones", endpointName) + ep.ObjectMeta.OwnerReferences = desiredOwnerRefs + c.setProcessName("updating %s endpoint", endpointName) + ep, err = c.KubeClient.Endpoints(c.Namespace).Update(context.TODO(), ep, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("could not update %s endpoint: %v", endpointName, err) + } + c.PatroniEndpoints[suffix] = ep + } + annotations := make(map[string]string) + maps.Copy(annotations, ep.Annotations) + // Patroni can add extra annotations so incl. current annotations in desired annotations + desiredAnnotations := c.annotationsSet(ep.Annotations) + if changed, _ := c.compareAnnotations(annotations, desiredAnnotations); changed { + patchData, err := metaAnnotationsPatch(desiredAnnotations) + if err != nil { + return fmt.Errorf("could not form patch for %s endpoint: %v", endpointName, err) + } + ep, err = c.KubeClient.Endpoints(c.Namespace).Patch(context.TODO(), endpointName, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}) + if err != nil { + return fmt.Errorf("could not patch annotations of %s endpoint: %v", endpointName, err) + } + c.PatroniEndpoints[suffix] = ep + } + } else if !k8sutil.ResourceNotFound(err) { + // if endpoint does not exist yet, Patroni should create it + return fmt.Errorf("could not get %s endpoint: %v", endpointName, err) + } + + return nil +} + +func (c *Cluster) syncPatroniService() error { + var ( + svc *v1.Service + err error + ) + serviceName := fmt.Sprintf("%s-%s", c.Name, Patroni) + c.setProcessName("syncing %s service", serviceName) + + if svc, err = c.KubeClient.Services(c.Namespace).Get(context.TODO(), serviceName, metav1.GetOptions{}); err == nil { + c.Services[Patroni] = svc + desiredOwnerRefs := c.ownerReferences() + if !reflect.DeepEqual(svc.ObjectMeta.OwnerReferences, desiredOwnerRefs) { + c.logger.Infof("new %s service's owner references do not match the current ones", serviceName) + svc.ObjectMeta.OwnerReferences = desiredOwnerRefs + c.setProcessName("updating %v service", serviceName) + svc, err = c.KubeClient.Services(c.Namespace).Update(context.TODO(), svc, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("could not update %s endpoint: %v", serviceName, err) + } + c.Services[Patroni] = svc + } + annotations := make(map[string]string) + maps.Copy(annotations, svc.Annotations) + // Patroni can add extra annotations so incl. current annotations in desired annotations + desiredAnnotations := c.annotationsSet(svc.Annotations) + if changed, _ := c.compareAnnotations(annotations, desiredAnnotations); changed { + patchData, err := metaAnnotationsPatch(desiredAnnotations) + if err != nil { + return fmt.Errorf("could not form patch for %s service: %v", serviceName, err) + } + svc, err = c.KubeClient.Services(c.Namespace).Patch(context.TODO(), serviceName, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}) + if err != nil { + return fmt.Errorf("could not patch annotations of %s service: %v", serviceName, err) + } + c.Services[Patroni] = svc + } + } else if !k8sutil.ResourceNotFound(err) { + // if config service does not exist yet, Patroni should create it + return fmt.Errorf("could not get %s service: %v", serviceName, err) + } + + return nil +} + func (c *Cluster) syncServices() error { for _, role := range []PostgresRole{Master, Replica} { c.logger.Debugf("syncing %s service", role) @@ -200,22 +365,17 @@ func (c *Cluster) syncService(role PostgresRole) error { if svc, err = c.KubeClient.Services(c.Namespace).Get(context.TODO(), c.serviceName(role), metav1.GetOptions{}); err == nil { c.Services[role] = svc desiredSvc := c.generateService(role, &c.Spec) - if match, reason := c.compareServices(svc, desiredSvc); !match { - c.logServiceChanges(role, svc, desiredSvc, false, reason) - updatedSvc, err := c.updateService(role, svc, desiredSvc) - if err != nil { - return fmt.Errorf("could not update %s service to match desired state: %v", role, err) - } - c.Services[role] = updatedSvc - c.logger.Infof("%s service %q is in the desired state now", role, util.NameFromMeta(desiredSvc.ObjectMeta)) + updatedSvc, err := c.updateService(role, svc, desiredSvc) + if err != nil { + return fmt.Errorf("could not update %s service to match desired state: %v", role, err) } + c.Services[role] = updatedSvc return nil } if !k8sutil.ResourceNotFound(err) { return fmt.Errorf("could not get %s service: %v", role, err) } // no existing service, create new one - c.Services[role] = nil c.logger.Infof("could not find the cluster's %s service", role) if svc, err = c.createService(role); err == nil { @@ -240,8 +400,28 @@ func (c *Cluster) syncEndpoint(role PostgresRole) error { ) c.setProcessName("syncing %s endpoint", role) - if ep, err = c.KubeClient.Endpoints(c.Namespace).Get(context.TODO(), c.endpointName(role), metav1.GetOptions{}); err == nil { - // TODO: No syncing of endpoints here, is this covered completely by updateService? + if ep, err = c.KubeClient.Endpoints(c.Namespace).Get(context.TODO(), c.serviceName(role), metav1.GetOptions{}); err == nil { + desiredEp := c.generateEndpoint(role, ep.Subsets) + // if owner references differ we update which would also change annotations + if !reflect.DeepEqual(ep.ObjectMeta.OwnerReferences, desiredEp.ObjectMeta.OwnerReferences) { + c.logger.Infof("new %s endpoints's owner references do not match the current ones", role) + c.setProcessName("updating %v endpoint", role) + ep, err = c.KubeClient.Endpoints(c.Namespace).Update(context.TODO(), desiredEp, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("could not update %s endpoint: %v", role, err) + } + } else { + if changed, _ := c.compareAnnotations(ep.Annotations, desiredEp.Annotations); changed { + patchData, err := metaAnnotationsPatch(desiredEp.Annotations) + if err != nil { + return fmt.Errorf("could not form patch for %s endpoint: %v", role, err) + } + ep, err = c.KubeClient.Endpoints(c.Namespace).Patch(context.TODO(), c.serviceName(role), types.MergePatchType, []byte(patchData), metav1.PatchOptions{}) + if err != nil { + return fmt.Errorf("could not patch annotations of %s endpoint: %v", role, err) + } + } + } c.Endpoints[role] = ep return nil } @@ -249,7 +429,6 @@ func (c *Cluster) syncEndpoint(role PostgresRole) error { return fmt.Errorf("could not get %s endpoint: %v", role, err) } // no existing endpoint, create new one - c.Endpoints[role] = nil c.logger.Infof("could not find the cluster's %s endpoint", role) if ep, err = c.createEndpoint(role); err == nil { @@ -259,7 +438,7 @@ func (c *Cluster) syncEndpoint(role PostgresRole) error { return fmt.Errorf("could not create missing %s endpoint: %v", role, err) } c.logger.Infof("%s endpoint %q already exists", role, util.NameFromMeta(ep.ObjectMeta)) - if ep, err = c.KubeClient.Endpoints(c.Namespace).Get(context.TODO(), c.endpointName(role), metav1.GetOptions{}); err != nil { + if ep, err = c.KubeClient.Endpoints(c.Namespace).Get(context.TODO(), c.serviceName(role), metav1.GetOptions{}); err != nil { return fmt.Errorf("could not fetch existing %s endpoint: %v", role, err) } } @@ -275,7 +454,8 @@ func (c *Cluster) syncPodDisruptionBudget(isUpdate bool) error { if pdb, err = c.KubeClient.PodDisruptionBudgets(c.Namespace).Get(context.TODO(), c.podDisruptionBudgetName(), metav1.GetOptions{}); err == nil { c.PodDisruptionBudget = pdb newPDB := c.generatePodDisruptionBudget() - if match, reason := k8sutil.SamePDB(pdb, newPDB); !match { + match, reason := c.comparePodDisruptionBudget(pdb, newPDB) + if !match { c.logPDBChanges(pdb, newPDB, isUpdate, reason) if err = c.updatePodDisruptionBudget(newPDB); err != nil { return err @@ -290,7 +470,6 @@ func (c *Cluster) syncPodDisruptionBudget(isUpdate bool) error { return fmt.Errorf("could not get pod disruption budget: %v", err) } // no existing pod disruption budget, create new one - c.PodDisruptionBudget = nil c.logger.Infof("could not find the cluster's pod disruption budget") if pdb, err = c.createPodDisruptionBudget(); err != nil { @@ -326,12 +505,12 @@ func (c *Cluster) syncStatefulSet() error { // NB: Be careful to consider the codepath that acts on podsRollingUpdateRequired before returning early. sset, err := c.KubeClient.StatefulSets(c.Namespace).Get(context.TODO(), c.statefulSetName(), metav1.GetOptions{}) + if err != nil && !k8sutil.ResourceNotFound(err) { + return fmt.Errorf("error during reading of statefulset: %v", err) + } + if err != nil { - if !k8sutil.ResourceNotFound(err) { - return fmt.Errorf("error during reading of statefulset: %v", err) - } // statefulset does not exist, try to re-create it - c.Statefulset = nil c.logger.Infof("cluster's statefulset does not exist") sset, err = c.createStatefulSet() @@ -354,6 +533,11 @@ func (c *Cluster) syncStatefulSet() error { c.logger.Infof("created missing statefulset %q", util.NameFromMeta(sset.ObjectMeta)) } else { + desiredSts, err := c.generateStatefulSet(&c.Spec) + if err != nil { + return fmt.Errorf("could not generate statefulset: %v", err) + } + c.logger.Debugf("syncing statefulsets") // check if there are still pods with a rolling update flag for _, pod := range pods { if c.getRollingUpdateFlagFromPod(&pod) { @@ -374,12 +558,21 @@ func (c *Cluster) syncStatefulSet() error { // statefulset is already there, make sure we use its definition in order to compare with the spec. c.Statefulset = sset - desiredSts, err := c.generateStatefulSet(&c.Spec) - if err != nil { - return fmt.Errorf("could not generate statefulset: %v", err) - } - cmp := c.compareStatefulSetWith(desiredSts) + if !cmp.rollingUpdate { + for _, pod := range pods { + if changed, _ := c.compareAnnotations(pod.Annotations, desiredSts.Spec.Template.Annotations); changed { + patchData, err := metaAnnotationsPatch(desiredSts.Spec.Template.Annotations) + if err != nil { + return fmt.Errorf("could not form patch for pod %q annotations: %v", pod.Name, err) + } + _, err = c.KubeClient.Pods(pod.Namespace).Patch(context.TODO(), pod.Name, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}) + if err != nil { + return fmt.Errorf("could not patch annotations for pod %q: %v", pod.Name, err) + } + } + } + } if !cmp.match { if cmp.rollingUpdate { podsToRecreate = make([]v1.Pod, 0) @@ -682,7 +875,7 @@ func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration(pod *v1.Pod, effectiv // check if specified slots exist in config and if they differ for slotName, desiredSlot := range desiredPatroniConfig.Slots { // only add slots specified in manifest to c.replicationSlots - for manifestSlotName, _ := range c.Spec.Patroni.Slots { + for manifestSlotName := range c.Spec.Patroni.Slots { if manifestSlotName == slotName { c.replicationSlots[slotName] = desiredSlot } @@ -934,14 +1127,32 @@ func (c *Cluster) updateSecret( userMap[userKey] = pwdUser } + if !reflect.DeepEqual(secret.ObjectMeta.OwnerReferences, generatedSecret.ObjectMeta.OwnerReferences) { + updateSecret = true + updateSecretMsg = fmt.Sprintf("secret %s owner references do not match the current ones", secretName) + secret.ObjectMeta.OwnerReferences = generatedSecret.ObjectMeta.OwnerReferences + } + if updateSecret { c.logger.Debugln(updateSecretMsg) - if _, err = c.KubeClient.Secrets(secret.Namespace).Update(context.TODO(), secret, metav1.UpdateOptions{}); err != nil { + if secret, err = c.KubeClient.Secrets(secret.Namespace).Update(context.TODO(), secret, metav1.UpdateOptions{}); err != nil { return fmt.Errorf("could not update secret %s: %v", secretName, err) } c.Secrets[secret.UID] = secret } + if changed, _ := c.compareAnnotations(secret.Annotations, generatedSecret.Annotations); changed { + patchData, err := metaAnnotationsPatch(generatedSecret.Annotations) + if err != nil { + return fmt.Errorf("could not form patch for secret %q annotations: %v", secret.Name, err) + } + secret, err = c.KubeClient.Secrets(secret.Namespace).Patch(context.TODO(), secret.Name, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}) + if err != nil { + return fmt.Errorf("could not patch annotations for secret %q: %v", secret.Name, err) + } + c.Secrets[secret.UID] = secret + } + return nil } @@ -1367,6 +1578,14 @@ func (c *Cluster) syncLogicalBackupJob() error { if err != nil { return fmt.Errorf("could not generate the desired logical backup job state: %v", err) } + if !reflect.DeepEqual(job.ObjectMeta.OwnerReferences, desiredJob.ObjectMeta.OwnerReferences) { + c.logger.Info("new logical backup job's owner references do not match the current ones") + job, err = c.KubeClient.CronJobs(job.Namespace).Update(context.TODO(), desiredJob, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("could not update owner references for logical backup job %q: %v", job.Name, err) + } + c.logger.Infof("logical backup job %s updated", c.getLogicalBackupJobName()) + } if match, reason := c.compareLogicalBackupJob(job, desiredJob); !match { c.logger.Infof("logical job %s is not in the desired state and needs to be updated", c.getLogicalBackupJobName(), @@ -1379,6 +1598,17 @@ func (c *Cluster) syncLogicalBackupJob() error { } c.logger.Info("the logical backup job is synced") } + if changed, _ := c.compareAnnotations(job.Annotations, desiredJob.Annotations); changed { + patchData, err := metaAnnotationsPatch(desiredJob.Annotations) + if err != nil { + return fmt.Errorf("could not form patch for the logical backup job %q: %v", jobName, err) + } + _, err = c.KubeClient.CronJobs(c.Namespace).Patch(context.TODO(), jobName, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}) + if err != nil { + return fmt.Errorf("could not patch annotations of the logical backup job %q: %v", jobName, err) + } + } + c.LogicalBackupJob = desiredJob return nil } if !k8sutil.ResourceNotFound(err) { diff --git a/pkg/cluster/types.go b/pkg/cluster/types.go index 1b4d0f389..8e9263d49 100644 --- a/pkg/cluster/types.go +++ b/pkg/cluster/types.go @@ -17,6 +17,7 @@ const ( // spilo roles Master PostgresRole = "master" Replica PostgresRole = "replica" + Patroni PostgresRole = "config" // roles returned by Patroni cluster endpoint Leader PostgresRole = "leader" diff --git a/pkg/cluster/util.go b/pkg/cluster/util.go index 2776ea92e..e36d0c175 100644 --- a/pkg/cluster/util.go +++ b/pkg/cluster/util.go @@ -176,6 +176,10 @@ func (c *Cluster) logPDBChanges(old, new *policyv1.PodDisruptionBudget, isUpdate } logNiceDiff(c.logger, old.Spec, new.Spec) + + if reason != "" { + c.logger.Infof("reason: %s", reason) + } } func logNiceDiff(log *logrus.Entry, old, new interface{}) { @@ -445,10 +449,6 @@ func (c *Cluster) _waitPodLabelsReady(anyReplica bool) error { return err } -func (c *Cluster) waitForAnyReplicaLabelReady() error { - return c._waitPodLabelsReady(true) -} - func (c *Cluster) waitForAllPodsLabelReady() error { return c._waitPodLabelsReady(false) } @@ -662,3 +662,24 @@ func parseResourceRequirements(resourcesRequirement v1.ResourceRequirements) (ac } return resources, nil } + +func isInMainternanceWindow(specMaintenanceWindows []acidv1.MaintenanceWindow) bool { + if len(specMaintenanceWindows) == 0 { + return true + } + now := time.Now() + currentDay := now.Weekday() + currentTime := now.Format("15:04") + + for _, window := range specMaintenanceWindows { + startTime := window.StartTime.Format("15:04") + endTime := window.EndTime.Format("15:04") + + if window.Everyday || window.Weekday == currentDay { + if currentTime >= startTime && currentTime <= endTime { + return true + } + } + } + return false +} diff --git a/pkg/cluster/util_test.go b/pkg/cluster/util_test.go index 5d8b92f2c..2cb755c6c 100644 --- a/pkg/cluster/util_test.go +++ b/pkg/cluster/util_test.go @@ -1,57 +1,299 @@ package cluster import ( + "bytes" "context" + "fmt" + "io" + "maps" + "net/http" + "reflect" "testing" + "time" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + "github.com/zalando/postgres-operator/mocks" 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/util" "github.com/zalando/postgres-operator/pkg/util/config" "github.com/zalando/postgres-operator/pkg/util/k8sutil" + "github.com/zalando/postgres-operator/pkg/util/patroni" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" k8sFake "k8s.io/client-go/kubernetes/fake" ) +var externalAnnotations = map[string]string{"existing": "annotation"} + +func mustParseTime(s string) metav1.Time { + v, err := time.Parse("15:04", s) + if err != nil { + panic(err) + } + + return metav1.Time{Time: v.UTC()} +} + func newFakeK8sAnnotationsClient() (k8sutil.KubernetesClient, *k8sFake.Clientset) { clientSet := k8sFake.NewSimpleClientset() acidClientSet := fakeacidv1.NewSimpleClientset() return k8sutil.KubernetesClient{ - PodDisruptionBudgetsGetter: clientSet.PolicyV1(), - ServicesGetter: clientSet.CoreV1(), - StatefulSetsGetter: clientSet.AppsV1(), - PostgresqlsGetter: acidClientSet.AcidV1(), + PodDisruptionBudgetsGetter: clientSet.PolicyV1(), + SecretsGetter: clientSet.CoreV1(), + ServicesGetter: clientSet.CoreV1(), + StatefulSetsGetter: clientSet.AppsV1(), + PostgresqlsGetter: acidClientSet.AcidV1(), + PersistentVolumeClaimsGetter: clientSet.CoreV1(), + PersistentVolumesGetter: clientSet.CoreV1(), + EndpointsGetter: clientSet.CoreV1(), + ConfigMapsGetter: clientSet.CoreV1(), + PodsGetter: clientSet.CoreV1(), + DeploymentsGetter: clientSet.AppsV1(), + CronJobsGetter: clientSet.BatchV1(), }, clientSet } -func TestInheritedAnnotations(t *testing.T) { - testName := "test inheriting annotations from manifest" - client, _ := newFakeK8sAnnotationsClient() - clusterName := "acid-test-cluster" - namespace := "default" - annotationValue := "acid" - role := Master +func clusterLabelsOptions(cluster *Cluster) metav1.ListOptions { + clusterLabel := labels.Set(map[string]string{cluster.OpConfig.ClusterNameLabel: cluster.Name}) + return metav1.ListOptions{ + LabelSelector: clusterLabel.String(), + } +} +func checkResourcesInheritedAnnotations(cluster *Cluster, resultAnnotations map[string]string) error { + clusterOptions := clusterLabelsOptions(cluster) + // helper functions + containsAnnotations := func(expected map[string]string, actual map[string]string, objName string, objType string) error { + if !util.MapContains(actual, expected) { + return fmt.Errorf("%s %v expected annotations %#v to be contained in %#v", objType, objName, expected, actual) + } + return nil + } + + updateAnnotations := func(annotations map[string]string) map[string]string { + result := make(map[string]string, 0) + for anno := range annotations { + if _, ok := externalAnnotations[anno]; !ok { + result[anno] = annotations[anno] + } + } + return result + } + + checkSts := func(annotations map[string]string) error { + stsList, err := cluster.KubeClient.StatefulSets(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err + } + stsAnnotations := updateAnnotations(annotations) + + for _, sts := range stsList.Items { + if err := containsAnnotations(stsAnnotations, sts.Annotations, sts.ObjectMeta.Name, "StatefulSet"); err != nil { + return err + } + // pod template + if err := containsAnnotations(stsAnnotations, sts.Spec.Template.Annotations, sts.ObjectMeta.Name, "StatefulSet pod template"); err != nil { + return err + } + // pvc template + if err := containsAnnotations(stsAnnotations, sts.Spec.VolumeClaimTemplates[0].Annotations, sts.ObjectMeta.Name, "StatefulSet pvc template"); err != nil { + return err + } + } + return nil + } + + checkPods := func(annotations map[string]string) error { + podList, err := cluster.KubeClient.Pods(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err + } + for _, pod := range podList.Items { + if err := containsAnnotations(annotations, pod.Annotations, pod.ObjectMeta.Name, "Pod"); err != nil { + return err + } + } + return nil + } + + checkSvc := func(annotations map[string]string) error { + svcList, err := cluster.KubeClient.Services(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err + } + for _, svc := range svcList.Items { + if err := containsAnnotations(annotations, svc.Annotations, svc.ObjectMeta.Name, "Service"); err != nil { + return err + } + } + return nil + } + + checkPdb := func(annotations map[string]string) error { + pdbList, err := cluster.KubeClient.PodDisruptionBudgets(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err + } + for _, pdb := range pdbList.Items { + if err := containsAnnotations(updateAnnotations(annotations), pdb.Annotations, pdb.ObjectMeta.Name, "Pod Disruption Budget"); err != nil { + return err + } + } + return nil + } + + checkPvc := func(annotations map[string]string) error { + pvcList, err := cluster.KubeClient.PersistentVolumeClaims(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err + } + for _, pvc := range pvcList.Items { + if err := containsAnnotations(annotations, pvc.Annotations, pvc.ObjectMeta.Name, "Volume claim"); err != nil { + return err + } + } + return nil + } + + checkPooler := func(annotations map[string]string) error { + for _, role := range []PostgresRole{Master, Replica} { + deploy, err := cluster.KubeClient.Deployments(namespace).Get(context.TODO(), cluster.connectionPoolerName(role), metav1.GetOptions{}) + if err != nil { + return err + } + if err := containsAnnotations(annotations, deploy.Annotations, deploy.Name, "Deployment"); err != nil { + return err + } + if err := containsAnnotations(updateAnnotations(annotations), deploy.Spec.Template.Annotations, deploy.Name, "Pooler pod template"); err != nil { + return err + } + } + return nil + } + + checkCronJob := func(annotations map[string]string) error { + cronJobList, err := cluster.KubeClient.CronJobs(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err + } + for _, cronJob := range cronJobList.Items { + if err := containsAnnotations(annotations, cronJob.Annotations, cronJob.ObjectMeta.Name, "Logical backup cron job"); err != nil { + return err + } + if err := containsAnnotations(updateAnnotations(annotations), cronJob.Spec.JobTemplate.Spec.Template.Annotations, cronJob.Name, "Logical backup cron job pod template"); err != nil { + return err + } + } + return nil + } + + checkSecrets := func(annotations map[string]string) error { + secretList, err := cluster.KubeClient.Secrets(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err + } + for _, secret := range secretList.Items { + if err := containsAnnotations(annotations, secret.Annotations, secret.Name, "Secret"); err != nil { + return err + } + } + return nil + } + + checkEndpoints := func(annotations map[string]string) error { + endpointsList, err := cluster.KubeClient.Endpoints(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err + } + for _, ep := range endpointsList.Items { + if err := containsAnnotations(annotations, ep.Annotations, ep.Name, "Endpoints"); err != nil { + return err + } + } + return nil + } + + checkConfigMaps := func(annotations map[string]string) error { + cmList, err := cluster.KubeClient.ConfigMaps(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err + } + for _, cm := range cmList.Items { + if err := containsAnnotations(annotations, cm.Annotations, cm.ObjectMeta.Name, "ConfigMap"); err != nil { + return err + } + } + return nil + } + + checkFuncs := []func(map[string]string) error{ + checkSts, checkPods, checkSvc, checkPdb, checkPooler, checkCronJob, checkPvc, checkSecrets, checkEndpoints, checkConfigMaps, + } + for _, f := range checkFuncs { + if err := f(resultAnnotations); err != nil { + return err + } + } + return nil +} + +func createPods(cluster *Cluster) []v1.Pod { + podsList := make([]v1.Pod, 0) + for i, role := range []PostgresRole{Master, Replica} { + podsList = append(podsList, v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-%d", clusterName, i), + Namespace: namespace, + Labels: map[string]string{ + "application": "spilo", + "cluster-name": clusterName, + "spilo-role": string(role), + }, + }, + }) + podsList = append(podsList, v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-pooler-%s", clusterName, role), + Namespace: namespace, + Labels: cluster.connectionPoolerLabels(role, true).MatchLabels, + }, + }) + } + + return podsList +} + +func newInheritedAnnotationsCluster(client k8sutil.KubernetesClient) (*Cluster, error) { pg := acidv1.Postgresql{ ObjectMeta: metav1.ObjectMeta{ Name: clusterName, Annotations: map[string]string{ - "owned-by": annotationValue, + "owned-by": "acid", + "foo": "bar", // should not be inherited }, }, Spec: acidv1.PostgresSpec{ + EnableConnectionPooler: boolToPointer(true), EnableReplicaConnectionPooler: boolToPointer(true), + EnableLogicalBackup: true, Volume: acidv1.Volume{ Size: "1Gi", }, + NumberOfInstances: 2, }, } - var cluster = New( + cluster := New( Config{ OpConfig: config.Config{ + PatroniAPICheckInterval: time.Duration(1), + PatroniAPICheckTimeout: time.Duration(5), + KubernetesUseConfigMaps: true, ConnectionPooler: config.ConnectionPooler{ ConnectionPoolerDefaultCPURequest: "100m", ConnectionPoolerDefaultCPULimit: "100m", @@ -59,85 +301,313 @@ func TestInheritedAnnotations(t *testing.T) { ConnectionPoolerDefaultMemoryLimit: "100Mi", NumberOfInstances: k8sutil.Int32ToPointer(1), }, + PDBNameFormat: "postgres-{cluster}-pdb", PodManagementPolicy: "ordered_ready", Resources: config.Resources{ - ClusterLabels: map[string]string{"application": "spilo"}, - ClusterNameLabel: "cluster-name", - DefaultCPURequest: "300m", - DefaultCPULimit: "300m", - DefaultMemoryRequest: "300Mi", - DefaultMemoryLimit: "300Mi", - InheritedAnnotations: []string{"owned-by"}, - PodRoleLabel: "spilo-role", + ClusterLabels: map[string]string{"application": "spilo"}, + ClusterNameLabel: "cluster-name", + DefaultCPURequest: "300m", + DefaultCPULimit: "300m", + DefaultMemoryRequest: "300Mi", + DefaultMemoryLimit: "300Mi", + InheritedAnnotations: []string{"owned-by"}, + PodRoleLabel: "spilo-role", + ResourceCheckInterval: time.Duration(testResourceCheckInterval), + ResourceCheckTimeout: time.Duration(testResourceCheckTimeout), + MinInstances: -1, + MaxInstances: -1, }, }, }, client, pg, logger, eventRecorder) - cluster.Name = clusterName cluster.Namespace = namespace - - // test annotationsSet function - inheritedAnnotations := cluster.annotationsSet(nil) - - listOptions := metav1.ListOptions{ - LabelSelector: cluster.labelsSet(false).String(), - } - - // check statefulset annotations _, err := cluster.createStatefulSet() - assert.NoError(t, err) + if err != nil { + return nil, err + } + _, err = cluster.createService(Master) + if err != nil { + return nil, err + } + _, err = cluster.createPodDisruptionBudget() + if err != nil { + return nil, err + } + _, err = cluster.createConnectionPooler(mockInstallLookupFunction) + if err != nil { + return nil, err + } + err = cluster.createLogicalBackupJob() + if err != nil { + return nil, err + } + pvcList := CreatePVCs(namespace, clusterName, cluster.labelsSet(false), 2, "1Gi") + for _, pvc := range pvcList.Items { + _, err = cluster.KubeClient.PersistentVolumeClaims(namespace).Create(context.TODO(), &pvc, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + } + podsList := createPods(cluster) + for _, pod := range podsList { + _, err = cluster.KubeClient.Pods(namespace).Create(context.TODO(), &pod, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + } - stsList, err := client.StatefulSets(namespace).List(context.TODO(), listOptions) - assert.NoError(t, err) + // resources which Patroni creates + if err = createPatroniResources(cluster); err != nil { + return nil, err + } + + return cluster, nil +} + +func createPatroniResources(cluster *Cluster) error { + patroniService := cluster.generateService(Replica, &pg.Spec) + patroniService.ObjectMeta.Name = cluster.serviceName(Patroni) + _, err := cluster.KubeClient.Services(namespace).Create(context.TODO(), patroniService, metav1.CreateOptions{}) + if err != nil { + return err + } + + for _, suffix := range patroniObjectSuffixes { + metadata := metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-%s", clusterName, suffix), + Namespace: namespace, + Annotations: map[string]string{ + "initialize": "123456789", + }, + Labels: cluster.labelsSet(false), + } + + if cluster.OpConfig.KubernetesUseConfigMaps { + configMap := v1.ConfigMap{ + ObjectMeta: metadata, + } + _, err := cluster.KubeClient.ConfigMaps(namespace).Create(context.TODO(), &configMap, metav1.CreateOptions{}) + if err != nil { + return err + } + } else { + endpoints := v1.Endpoints{ + ObjectMeta: metadata, + } + _, err := cluster.KubeClient.Endpoints(namespace).Create(context.TODO(), &endpoints, metav1.CreateOptions{}) + if err != nil { + return err + } + } + } + + return nil +} + +func annotateResources(cluster *Cluster) error { + clusterOptions := clusterLabelsOptions(cluster) + patchData, err := metaAnnotationsPatch(externalAnnotations) + if err != nil { + return err + } + + stsList, err := cluster.KubeClient.StatefulSets(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err + } for _, sts := range stsList.Items { - if !(util.MapContains(sts.ObjectMeta.Annotations, inheritedAnnotations)) { - t.Errorf("%s: StatefulSet %v not inherited annotations %#v, got %#v", testName, sts.ObjectMeta.Name, inheritedAnnotations, sts.ObjectMeta.Annotations) - } - // pod template - if !(util.MapContains(sts.Spec.Template.ObjectMeta.Annotations, inheritedAnnotations)) { - t.Errorf("%s: pod template %v not inherited annotations %#v, got %#v", testName, sts.ObjectMeta.Name, inheritedAnnotations, sts.ObjectMeta.Annotations) - } - // pvc template - if !(util.MapContains(sts.Spec.VolumeClaimTemplates[0].Annotations, inheritedAnnotations)) { - t.Errorf("%s: PVC template %v not inherited annotations %#v, got %#v", testName, sts.ObjectMeta.Name, inheritedAnnotations, sts.ObjectMeta.Annotations) + sts.Annotations = externalAnnotations + if _, err = cluster.KubeClient.StatefulSets(namespace).Patch(context.TODO(), sts.Name, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}); err != nil { + return err } } - // check service annotations - cluster.createService(Master) - svcList, err := client.Services(namespace).List(context.TODO(), listOptions) - assert.NoError(t, err) + podList, err := cluster.KubeClient.Pods(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err + } + for _, pod := range podList.Items { + pod.Annotations = externalAnnotations + if _, err = cluster.KubeClient.Pods(namespace).Patch(context.TODO(), pod.Name, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}); err != nil { + return err + } + } + + svcList, err := cluster.KubeClient.Services(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err + } for _, svc := range svcList.Items { - if !(util.MapContains(svc.ObjectMeta.Annotations, inheritedAnnotations)) { - t.Errorf("%s: Service %v not inherited annotations %#v, got %#v", testName, svc.ObjectMeta.Name, inheritedAnnotations, svc.ObjectMeta.Annotations) + svc.Annotations = externalAnnotations + if _, err = cluster.KubeClient.Services(namespace).Patch(context.TODO(), svc.Name, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}); err != nil { + return err } } - // check pod disruption budget annotations - cluster.createPodDisruptionBudget() - pdbList, err := client.PodDisruptionBudgets(namespace).List(context.TODO(), listOptions) - assert.NoError(t, err) + pdbList, err := cluster.KubeClient.PodDisruptionBudgets(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err + } for _, pdb := range pdbList.Items { - if !(util.MapContains(pdb.ObjectMeta.Annotations, inheritedAnnotations)) { - t.Errorf("%s: Pod Disruption Budget %v not inherited annotations %#v, got %#v", testName, pdb.ObjectMeta.Name, inheritedAnnotations, pdb.ObjectMeta.Annotations) + pdb.Annotations = externalAnnotations + _, err = cluster.KubeClient.PodDisruptionBudgets(namespace).Patch(context.TODO(), pdb.Name, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}) + if err != nil { + return err } } - // check pooler deployment annotations - cluster.ConnectionPooler = map[PostgresRole]*ConnectionPoolerObjects{} - cluster.ConnectionPooler[role] = &ConnectionPoolerObjects{ - Name: cluster.connectionPoolerName(role), - ClusterName: cluster.Name, - Namespace: cluster.Namespace, - Role: role, + cronJobList, err := cluster.KubeClient.CronJobs(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err } - deploy, err := cluster.generateConnectionPoolerDeployment(cluster.ConnectionPooler[role]) + for _, cronJob := range cronJobList.Items { + cronJob.Annotations = externalAnnotations + _, err = cluster.KubeClient.CronJobs(namespace).Patch(context.TODO(), cronJob.Name, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}) + if err != nil { + return err + } + } + + pvcList, err := cluster.KubeClient.PersistentVolumeClaims(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err + } + for _, pvc := range pvcList.Items { + pvc.Annotations = externalAnnotations + if _, err = cluster.KubeClient.PersistentVolumeClaims(namespace).Patch(context.TODO(), pvc.Name, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}); err != nil { + return err + } + } + + for _, role := range []PostgresRole{Master, Replica} { + deploy, err := cluster.KubeClient.Deployments(namespace).Get(context.TODO(), cluster.connectionPoolerName(role), metav1.GetOptions{}) + if err != nil { + return err + } + deploy.Annotations = externalAnnotations + if _, err = cluster.KubeClient.Deployments(namespace).Patch(context.TODO(), deploy.Name, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}); err != nil { + return err + } + } + + secrets, err := cluster.KubeClient.Secrets(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err + } + for _, secret := range secrets.Items { + secret.Annotations = externalAnnotations + if _, err = cluster.KubeClient.Secrets(namespace).Patch(context.TODO(), secret.Name, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}); err != nil { + return err + } + } + + endpoints, err := cluster.KubeClient.Endpoints(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err + } + for _, ep := range endpoints.Items { + ep.Annotations = externalAnnotations + if _, err = cluster.KubeClient.Endpoints(namespace).Patch(context.TODO(), ep.Name, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}); err != nil { + return err + } + } + + configMaps, err := cluster.KubeClient.ConfigMaps(namespace).List(context.TODO(), clusterOptions) + if err != nil { + return err + } + for _, cm := range configMaps.Items { + cm.Annotations = externalAnnotations + if _, err = cluster.KubeClient.ConfigMaps(namespace).Patch(context.TODO(), cm.Name, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}); err != nil { + return err + } + } + + return nil +} + +func TestInheritedAnnotations(t *testing.T) { + // mocks + ctrl := gomock.NewController(t) + defer ctrl.Finish() + client, _ := newFakeK8sAnnotationsClient() + mockClient := mocks.NewMockHTTPClient(ctrl) + + cluster, err := newInheritedAnnotationsCluster(client) assert.NoError(t, err) - if !(util.MapContains(deploy.ObjectMeta.Annotations, inheritedAnnotations)) { - t.Errorf("%s: Deployment %v not inherited annotations %#v, got %#v", testName, deploy.ObjectMeta.Name, inheritedAnnotations, deploy.ObjectMeta.Annotations) + configJson := `{"postgresql": {"parameters": {"log_min_duration_statement": 200, "max_connections": 50}}}, "ttl": 20}` + response := http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewReader([]byte(configJson))), } + mockClient.EXPECT().Do(gomock.Any()).Return(&response, nil).AnyTimes() + cluster.patroni = patroni.New(patroniLogger, mockClient) + err = cluster.Sync(&cluster.Postgresql) + assert.NoError(t, err) + + filterLabels := cluster.labelsSet(false) + + // Finally, tests! + result := map[string]string{"owned-by": "acid"} + assert.True(t, reflect.DeepEqual(result, cluster.annotationsSet(nil))) + + // 1. Check initial state + err = checkResourcesInheritedAnnotations(cluster, result) + assert.NoError(t, err) + + // 2. Check annotation value change + + // 2.1 Sync event + newSpec := cluster.Postgresql.DeepCopy() + newSpec.Annotations["owned-by"] = "fooSync" + result["owned-by"] = "fooSync" + + err = cluster.Sync(newSpec) + assert.NoError(t, err) + err = checkResourcesInheritedAnnotations(cluster, result) + assert.NoError(t, err) + + // + existing PVC without annotations + cluster.KubeClient.PersistentVolumeClaims(namespace).Create(context.TODO(), &CreatePVCs(namespace, clusterName, filterLabels, 3, "1Gi").Items[2], metav1.CreateOptions{}) + err = cluster.Sync(newSpec) + assert.NoError(t, err) + err = checkResourcesInheritedAnnotations(cluster, result) + assert.NoError(t, err) + + // 2.2 Update event + newSpec = cluster.Postgresql.DeepCopy() + newSpec.Annotations["owned-by"] = "fooUpdate" + result["owned-by"] = "fooUpdate" + // + new PVC + cluster.KubeClient.PersistentVolumeClaims(namespace).Create(context.TODO(), &CreatePVCs(namespace, clusterName, filterLabels, 4, "1Gi").Items[3], metav1.CreateOptions{}) + + err = cluster.Update(cluster.Postgresql.DeepCopy(), newSpec) + assert.NoError(t, err) + + err = checkResourcesInheritedAnnotations(cluster, result) + assert.NoError(t, err) + + // 3. Change from ConfigMaps to Endpoints + err = cluster.deletePatroniResources() + assert.NoError(t, err) + cluster.OpConfig.KubernetesUseConfigMaps = false + err = createPatroniResources(cluster) + assert.NoError(t, err) + err = cluster.Sync(newSpec.DeepCopy()) + assert.NoError(t, err) + err = checkResourcesInheritedAnnotations(cluster, result) + assert.NoError(t, err) + + // 4. Existing annotations (should not be removed) + err = annotateResources(cluster) + assert.NoError(t, err) + maps.Copy(result, externalAnnotations) + err = cluster.Sync(newSpec.DeepCopy()) + assert.NoError(t, err) + err = checkResourcesInheritedAnnotations(cluster, result) + assert.NoError(t, err) } func Test_trimCronjobName(t *testing.T) { @@ -179,3 +649,65 @@ func Test_trimCronjobName(t *testing.T) { }) } } + +func TestIsInMaintenanceWindow(t *testing.T) { + now := time.Now() + futureTimeStart := now.Add(1 * time.Hour) + futureTimeStartFormatted := futureTimeStart.Format("15:04") + futureTimeEnd := now.Add(2 * time.Hour) + futureTimeEndFormatted := futureTimeEnd.Format("15:04") + + tests := []struct { + name string + windows []acidv1.MaintenanceWindow + expected bool + }{ + { + name: "no maintenance windows", + windows: nil, + expected: true, + }, + { + name: "maintenance windows with everyday", + windows: []acidv1.MaintenanceWindow{ + { + Everyday: true, + StartTime: mustParseTime("00:00"), + EndTime: mustParseTime("23:59"), + }, + }, + expected: true, + }, + { + name: "maintenance windows with weekday", + windows: []acidv1.MaintenanceWindow{ + { + Weekday: now.Weekday(), + StartTime: mustParseTime("00:00"), + EndTime: mustParseTime("23:59"), + }, + }, + expected: true, + }, + { + name: "maintenance windows with future interval time", + windows: []acidv1.MaintenanceWindow{ + { + Weekday: now.Weekday(), + StartTime: mustParseTime(futureTimeStartFormatted), + EndTime: mustParseTime(futureTimeEndFormatted), + }, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cluster.Spec.MaintenanceWindows = tt.windows + if isInMainternanceWindow(cluster.Spec.MaintenanceWindows) != tt.expected { + t.Errorf("Expected isInMainternanceWindow to return %t", tt.expected) + } + }) + } +} diff --git a/pkg/cluster/volumes.go b/pkg/cluster/volumes.go index 1a4c7c73f..2646acbb7 100644 --- a/pkg/cluster/volumes.go +++ b/pkg/cluster/volumes.go @@ -9,9 +9,9 @@ import ( v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "github.com/aws/aws-sdk-go/aws" - acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" "github.com/zalando/postgres-operator/pkg/spec" "github.com/zalando/postgres-operator/pkg/util" "github.com/zalando/postgres-operator/pkg/util/constants" @@ -42,18 +42,14 @@ func (c *Cluster) syncVolumes() error { c.logger.Errorf("errors occured during EBS volume adjustments: %v", err) } } + } - // resize pvc to adjust filesystem size until better K8s support - if err = c.syncVolumeClaims(); err != nil { - err = fmt.Errorf("could not sync persistent volume claims: %v", err) - return err - } - } else if c.OpConfig.StorageResizeMode == "pvc" { - if err = c.syncVolumeClaims(); err != nil { - err = fmt.Errorf("could not sync persistent volume claims: %v", err) - return err - } - } else if c.OpConfig.StorageResizeMode == "ebs" { + if err = c.syncVolumeClaims(); err != nil { + err = fmt.Errorf("could not sync persistent volume claims: %v", err) + return err + } + + if c.OpConfig.StorageResizeMode == "ebs" { // potentially enlarge volumes before changing the statefulset. By doing that // in this order we make sure the operator is not stuck waiting for a pod that // cannot start because it ran out of disk space. @@ -64,8 +60,6 @@ func (c *Cluster) syncVolumes() error { err = fmt.Errorf("could not sync persistent volumes: %v", err) return err } - } else { - c.logger.Infof("Storage resize is disabled (storage_resize_mode is off). Skipping volume sync.") } return nil @@ -187,18 +181,57 @@ func (c *Cluster) populateVolumeMetaData() error { func (c *Cluster) syncVolumeClaims() error { c.setProcessName("syncing volume claims") - needsResizing, err := c.volumeClaimsNeedResizing(c.Spec.Volume) + ignoreResize := false + + if c.OpConfig.StorageResizeMode == "off" || c.OpConfig.StorageResizeMode == "ebs" { + ignoreResize = true + c.logger.Debugf("Storage resize mode is set to %q. Skipping volume size sync of PVCs.", c.OpConfig.StorageResizeMode) + } + + newSize, err := resource.ParseQuantity(c.Spec.Volume.Size) if err != nil { - return fmt.Errorf("could not compare size of the volume claims: %v", err) + return fmt.Errorf("could not parse volume size from the manifest: %v", err) } + manifestSize := quantityToGigabyte(newSize) - if !needsResizing { - c.logger.Infof("volume claims do not require changes") - return nil + pvcs, err := c.listPersistentVolumeClaims() + if err != nil { + return fmt.Errorf("could not receive persistent volume claims: %v", err) } + for _, pvc := range pvcs { + needsUpdate := false + currentSize := quantityToGigabyte(pvc.Spec.Resources.Requests[v1.ResourceStorage]) + if !ignoreResize && currentSize != manifestSize { + if currentSize < manifestSize { + pvc.Spec.Resources.Requests[v1.ResourceStorage] = newSize + needsUpdate = true + c.logger.Debugf("persistent volume claim for volume %q needs to be resized", pvc.Name) + } else { + c.logger.Warningf("cannot shrink persistent volume") + } + } - if err := c.resizeVolumeClaims(c.Spec.Volume); err != nil { - return fmt.Errorf("could not sync volume claims: %v", err) + if needsUpdate { + c.logger.Debugf("updating persistent volume claim definition for volume %q", pvc.Name) + if _, err := c.KubeClient.PersistentVolumeClaims(pvc.Namespace).Update(context.TODO(), &pvc, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("could not update persistent volume claim: %q", err) + } + c.logger.Debugf("successfully updated persistent volume claim %q", pvc.Name) + } else { + c.logger.Debugf("volume claim for volume %q do not require updates", pvc.Name) + } + + newAnnotations := c.annotationsSet(nil) + if changed, _ := c.compareAnnotations(pvc.Annotations, newAnnotations); changed { + patchData, err := metaAnnotationsPatch(newAnnotations) + if err != nil { + return fmt.Errorf("could not form patch for the persistent volume claim for volume %q: %v", pvc.Name, err) + } + _, err = c.KubeClient.PersistentVolumeClaims(pvc.Namespace).Patch(context.TODO(), pvc.Name, types.MergePatchType, []byte(patchData), metav1.PatchOptions{}) + if err != nil { + return fmt.Errorf("could not patch annotations of the persistent volume claim for volume %q: %v", pvc.Name, err) + } + } } c.logger.Infof("volume claims have been synced successfully") @@ -261,35 +294,6 @@ func (c *Cluster) deletePersistentVolumeClaims() error { return nil } -func (c *Cluster) resizeVolumeClaims(newVolume acidv1.Volume) error { - c.logger.Debugln("resizing PVCs") - pvcs, err := c.listPersistentVolumeClaims() - if err != nil { - return err - } - newQuantity, err := resource.ParseQuantity(newVolume.Size) - if err != nil { - return fmt.Errorf("could not parse volume size: %v", err) - } - newSize := quantityToGigabyte(newQuantity) - for _, pvc := range pvcs { - volumeSize := quantityToGigabyte(pvc.Spec.Resources.Requests[v1.ResourceStorage]) - if volumeSize >= newSize { - if volumeSize > newSize { - c.logger.Warningf("cannot shrink persistent volume") - } - continue - } - pvc.Spec.Resources.Requests[v1.ResourceStorage] = newQuantity - c.logger.Debugf("updating persistent volume claim definition for volume %q", pvc.Name) - if _, err := c.KubeClient.PersistentVolumeClaims(pvc.Namespace).Update(context.TODO(), &pvc, metav1.UpdateOptions{}); err != nil { - return fmt.Errorf("could not update persistent volume claim: %q", err) - } - c.logger.Debugf("successfully updated persistent volume claim %q", pvc.Name) - } - return nil -} - func (c *Cluster) listPersistentVolumes() ([]*v1.PersistentVolume, error) { result := make([]*v1.PersistentVolume, 0) @@ -406,25 +410,6 @@ func (c *Cluster) resizeVolumes() error { return nil } -func (c *Cluster) volumeClaimsNeedResizing(newVolume acidv1.Volume) (bool, error) { - newSize, err := resource.ParseQuantity(newVolume.Size) - manifestSize := quantityToGigabyte(newSize) - if err != nil { - return false, fmt.Errorf("could not parse volume size from the manifest: %v", err) - } - pvcs, err := c.listPersistentVolumeClaims() - if err != nil { - return false, fmt.Errorf("could not receive persistent volume claims: %v", err) - } - for _, pvc := range pvcs { - currentSize := quantityToGigabyte(pvc.Spec.Resources.Requests[v1.ResourceStorage]) - if currentSize != manifestSize { - return true, nil - } - } - return false, nil -} - func (c *Cluster) volumesNeedResizing() (bool, error) { newQuantity, _ := resource.ParseQuantity(c.Spec.Volume.Size) newSize := quantityToGigabyte(newQuantity) diff --git a/pkg/cluster/volumes_test.go b/pkg/cluster/volumes_test.go index 4ef94fcfb..329224893 100644 --- a/pkg/cluster/volumes_test.go +++ b/pkg/cluster/volumes_test.go @@ -74,6 +74,7 @@ func TestResizeVolumeClaim(t *testing.T) { cluster.Name = clusterName cluster.Namespace = namespace filterLabels := cluster.labelsSet(false) + cluster.Spec.Volume.Size = newVolumeSize // define and create PVCs for 1Gi volumes pvcList := CreatePVCs(namespace, clusterName, filterLabels, 2, "1Gi") @@ -85,7 +86,7 @@ func TestResizeVolumeClaim(t *testing.T) { } // test resizing - cluster.resizeVolumeClaims(acidv1.Volume{Size: newVolumeSize}) + cluster.syncVolumes() pvcs, err := cluster.listPersistentVolumeClaims() assert.NoError(t, err) diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 88f1d73c0..78e752f1d 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -39,7 +39,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.EnableTeamIdClusternamePrefix = fromCRD.EnableTeamIdClusternamePrefix result.EtcdHost = fromCRD.EtcdHost result.KubernetesUseConfigMaps = fromCRD.KubernetesUseConfigMaps - result.DockerImage = util.Coalesce(fromCRD.DockerImage, "ghcr.io/zalando/spilo-16:3.2-p3") + result.DockerImage = util.Coalesce(fromCRD.DockerImage, "ghcr.io/zalando/spilo-16:3.3-p1") result.Workers = util.CoalesceUInt32(fromCRD.Workers, 8) result.MinInstances = fromCRD.MinInstances result.MaxInstances = fromCRD.MaxInstances @@ -60,12 +60,13 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.PasswordRotationUserRetention = util.CoalesceUInt32(fromCRD.PostgresUsersConfiguration.DeepCopy().PasswordRotationUserRetention, 180) // major version upgrade config - result.MajorVersionUpgradeMode = util.Coalesce(fromCRD.MajorVersionUpgrade.MajorVersionUpgradeMode, "off") + result.MajorVersionUpgradeMode = util.Coalesce(fromCRD.MajorVersionUpgrade.MajorVersionUpgradeMode, "manual") result.MajorVersionUpgradeTeamAllowList = fromCRD.MajorVersionUpgrade.MajorVersionUpgradeTeamAllowList result.MinimalMajorVersion = util.Coalesce(fromCRD.MajorVersionUpgrade.MinimalMajorVersion, "12") result.TargetMajorVersion = util.Coalesce(fromCRD.MajorVersionUpgrade.TargetMajorVersion, "16") // kubernetes config + result.EnableOwnerReferences = util.CoalesceBool(fromCRD.Kubernetes.EnableOwnerReferences, util.False()) result.CustomPodAnnotations = fromCRD.Kubernetes.CustomPodAnnotations result.PodServiceAccountName = util.Coalesce(fromCRD.Kubernetes.PodServiceAccountName, "postgres-pod") result.PodServiceAccountDefinition = fromCRD.Kubernetes.PodServiceAccountDefinition @@ -173,13 +174,13 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur 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.AdditionalSecretMountPath = fromCRD.AWSGCP.AdditionalSecretMountPath result.EnableEBSGp3Migration = fromCRD.AWSGCP.EnableEBSGp3Migration result.EnableEBSGp3MigrationMaxSize = util.CoalesceInt64(fromCRD.AWSGCP.EnableEBSGp3MigrationMaxSize, 1000) // logical backup config result.LogicalBackupSchedule = util.Coalesce(fromCRD.LogicalBackup.Schedule, "30 00 * * *") - result.LogicalBackupDockerImage = util.Coalesce(fromCRD.LogicalBackup.DockerImage, "ghcr.io/zalando/postgres-operator/logical-backup:v1.12.2") + result.LogicalBackupDockerImage = util.Coalesce(fromCRD.LogicalBackup.DockerImage, "ghcr.io/zalando/postgres-operator/logical-backup:v1.13.0") result.LogicalBackupProvider = util.Coalesce(fromCRD.LogicalBackup.BackupProvider, "s3") result.LogicalBackupAzureStorageAccountName = fromCRD.LogicalBackup.AzureStorageAccountName result.LogicalBackupAzureStorageAccountKey = fromCRD.LogicalBackup.AzureStorageAccountKey diff --git a/pkg/controller/postgresql.go b/pkg/controller/postgresql.go index accc345ad..4466080b7 100644 --- a/pkg/controller/postgresql.go +++ b/pkg/controller/postgresql.go @@ -384,10 +384,6 @@ func (c *Controller) warnOnDeprecatedPostgreSQLSpecParameters(spec *acidv1.Postg c.logger.Warningf("parameter %q is deprecated. Consider setting %q instead", deprecated, replacement) } - noeffect := func(param string, explanation string) { - c.logger.Warningf("parameter %q takes no effect. %s", param, explanation) - } - if spec.UseLoadBalancer != nil { deprecate("useLoadBalancer", "enableMasterLoadBalancer") } @@ -395,10 +391,6 @@ func (c *Controller) warnOnDeprecatedPostgreSQLSpecParameters(spec *acidv1.Postg deprecate("replicaLoadBalancer", "enableReplicaLoadBalancer") } - if len(spec.MaintenanceWindows) > 0 { - noeffect("maintenanceWindows", "Not implemented.") - } - if (spec.UseLoadBalancer != nil || spec.ReplicaLoadBalancer != nil) && (spec.EnableReplicaLoadBalancer != nil || spec.EnableMasterLoadBalancer != nil) { c.logger.Warnf("both old and new load balancer parameters are present in the manifest, ignoring old ones") @@ -454,19 +446,22 @@ func (c *Controller) queueClusterEvent(informerOldSpec, informerNewSpec *acidv1. clusterError = informerNewSpec.Error } - // only allow deletion if delete annotations are set and conditions are met if eventType == EventDelete { - if err := c.meetsClusterDeleteAnnotations(informerOldSpec); err != nil { - c.logger.WithField("cluster-name", clusterName).Warnf( - "ignoring %q event for cluster %q - manifest does not fulfill delete requirements: %s", eventType, clusterName, err) - c.logger.WithField("cluster-name", clusterName).Warnf( - "please, recreate Postgresql resource %q and set annotations to delete properly", clusterName) - if currentManifest, marshalErr := json.Marshal(informerOldSpec); marshalErr != nil { - c.logger.WithField("cluster-name", clusterName).Warnf("could not marshal current manifest:\n%+v", informerOldSpec) - } else { - c.logger.WithField("cluster-name", clusterName).Warnf("%s\n", string(currentManifest)) + // when owner references are used operator cannot block deletion + if c.opConfig.EnableOwnerReferences == nil || !*c.opConfig.EnableOwnerReferences { + // only allow deletion if delete annotations are set and conditions are met + if err := c.meetsClusterDeleteAnnotations(informerOldSpec); err != nil { + c.logger.WithField("cluster-name", clusterName).Warnf( + "ignoring %q event for cluster %q - manifest does not fulfill delete requirements: %s", eventType, clusterName, err) + c.logger.WithField("cluster-name", clusterName).Warnf( + "please, recreate Postgresql resource %q and set annotations to delete properly", clusterName) + if currentManifest, marshalErr := json.Marshal(informerOldSpec); marshalErr != nil { + c.logger.WithField("cluster-name", clusterName).Warnf("could not marshal current manifest:\n%+v", informerOldSpec) + } else { + c.logger.WithField("cluster-name", clusterName).Warnf("%s\n", string(currentManifest)) + } + return } - return } } diff --git a/pkg/controller/util.go b/pkg/controller/util.go index bd1e65d02..5a3b23edc 100644 --- a/pkg/controller/util.go +++ b/pkg/controller/util.go @@ -80,7 +80,7 @@ func (c *Controller) createOperatorCRD(desiredCrd *apiextv1.CustomResourceDefini c.logger.Infof("customResourceDefinition %q has been registered", crd.Name) } - return wait.Poll(c.config.CRDReadyWaitInterval, c.config.CRDReadyWaitTimeout, func() (bool, error) { + return wait.PollUntilContextTimeout(context.TODO(), c.config.CRDReadyWaitInterval, c.config.CRDReadyWaitTimeout, false, func(ctx context.Context) (bool, error) { c, err := c.KubeClient.CustomResourceDefinitions().Get(context.TODO(), desiredCrd.Name, metav1.GetOptions{}) if err != nil { return false, err diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 829c1d19e..4c7b8db10 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -25,6 +25,7 @@ type CRD struct { // Resources describes kubernetes resource specific configuration parameters type Resources struct { + EnableOwnerReferences *bool `name:"enable_owner_references" default:"false"` ResourceCheckInterval time.Duration `name:"resource_check_interval" default:"3s"` ResourceCheckTimeout time.Duration `name:"resource_check_timeout" default:"10m"` PodLabelWaitTimeout time.Duration `name:"pod_label_wait_timeout" default:"10m"` @@ -126,7 +127,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:"ghcr.io/zalando/postgres-operator/logical-backup:v1.12.2"` + LogicalBackupDockerImage string `name:"logical_backup_docker_image" default:"ghcr.io/zalando/postgres-operator/logical-backup:v1.13.0"` LogicalBackupProvider string `name:"logical_backup_provider" default:"s3"` LogicalBackupAzureStorageAccountName string `name:"logical_backup_azure_storage_account_name" default:""` LogicalBackupAzureStorageContainer string `name:"logical_backup_azure_storage_container" default:""` @@ -174,7 +175,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:"ghcr.io/zalando/spilo-16:3.2-p3"` + DockerImage string `name:"docker_image" default:"ghcr.io/zalando/spilo-16:3.3-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"` @@ -191,7 +192,7 @@ type Config struct { 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"` + AdditionalSecretMountPath string `name:"additional_secret_mount_path"` EnableEBSGp3Migration bool `name:"enable_ebs_gp3_migration" default:"false"` EnableEBSGp3MigrationMaxSize int64 `name:"enable_ebs_gp3_migration_max_size" default:"1000"` DebugLogging bool `name:"debug_logging" default:"true"` @@ -243,7 +244,7 @@ type Config struct { EnablePgVersionEnvVar bool `name:"enable_pgversion_env_var" default:"true"` EnableSpiloWalPathCompat bool `name:"enable_spilo_wal_path_compat" default:"false"` EnableTeamIdClusternamePrefix bool `name:"enable_team_id_clustername_prefix" default:"false"` - MajorVersionUpgradeMode string `name:"major_version_upgrade_mode" default:"off"` + MajorVersionUpgradeMode string `name:"major_version_upgrade_mode" default:"manual"` MajorVersionUpgradeTeamAllowList []string `name:"major_version_upgrade_team_allow_list" default:""` MinimalMajorVersion string `name:"minimal_major_version" default:"12"` TargetMajorVersion string `name:"target_major_version" default:"16"` diff --git a/pkg/util/k8sutil/k8sutil.go b/pkg/util/k8sutil/k8sutil.go index 66c30dede..7ae402fe3 100644 --- a/pkg/util/k8sutil/k8sutil.go +++ b/pkg/util/k8sutil/k8sutil.go @@ -3,7 +3,6 @@ package k8sutil import ( "context" "fmt" - "reflect" b64 "encoding/base64" "encoding/json" @@ -17,9 +16,9 @@ import ( "github.com/zalando/postgres-operator/pkg/spec" apiappsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" - apipolicyv1 "k8s.io/api/policy/v1" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" - apiextv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" + apiextv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -61,7 +60,7 @@ type KubernetesClient struct { appsv1.DeploymentsGetter rbacv1.RoleBindingsGetter policyv1.PodDisruptionBudgetsGetter - apiextv1.CustomResourceDefinitionsGetter + apiextv1client.CustomResourceDefinitionsGetter clientbatchv1.CronJobsGetter acidv1.OperatorConfigurationsGetter acidv1.PostgresTeamsGetter @@ -73,6 +72,13 @@ type KubernetesClient struct { Zalandov1ClientSet *zalandoclient.Clientset } +type mockCustomResourceDefinition struct { + apiextv1client.CustomResourceDefinitionInterface +} + +type MockCustomResourceDefinitionsGetter struct { +} + type mockSecret struct { corev1.SecretInterface } @@ -242,15 +248,16 @@ func (client *KubernetesClient) SetFinalizer(clusterName spec.NamespacedName, pg return updatedPg, nil } -// SamePDB compares the PodDisruptionBudgets -func SamePDB(cur, new *apipolicyv1.PodDisruptionBudget) (match bool, reason string) { - //TODO: improve comparison - match = reflect.DeepEqual(new.Spec, cur.Spec) - if !match { - reason = "new PDB spec does not match the current one" - } +func (c *mockCustomResourceDefinition) Get(ctx context.Context, name string, options metav1.GetOptions) (*apiextv1.CustomResourceDefinition, error) { + return &apiextv1.CustomResourceDefinition{}, nil +} - return +func (c *mockCustomResourceDefinition) Create(ctx context.Context, crd *apiextv1.CustomResourceDefinition, options metav1.CreateOptions) (*apiextv1.CustomResourceDefinition, error) { + return &apiextv1.CustomResourceDefinition{}, nil +} + +func (mock *MockCustomResourceDefinitionsGetter) CustomResourceDefinitions() apiextv1client.CustomResourceDefinitionInterface { + return &mockCustomResourceDefinition{} } func (c *mockSecret) Get(ctx context.Context, name string, options metav1.GetOptions) (*v1.Secret, error) { @@ -457,6 +464,8 @@ func NewMockKubernetesClient() KubernetesClient { ConfigMapsGetter: &MockConfigMapsGetter{}, DeploymentsGetter: &MockDeploymentGetter{}, ServicesGetter: &MockServiceGetter{}, + + CustomResourceDefinitionsGetter: &MockCustomResourceDefinitionsGetter{}, } } diff --git a/pkg/util/util.go b/pkg/util/util.go index fb1217d1f..4b3aafc63 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -35,7 +35,7 @@ const ( var passwordChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") func init() { - rand.Seed(time.Now().Unix()) + rand.New(rand.NewSource(time.Now().Unix())) } // helper function to get bool pointers diff --git a/ui/app/package.json b/ui/app/package.json index 076aa7875..e96ee77dc 100644 --- a/ui/app/package.json +++ b/ui/app/package.json @@ -1,6 +1,6 @@ { "name": "postgres-operator-ui", - "version": "1.12.2", + "version": "1.13.0", "description": "PostgreSQL Operator UI", "main": "src/app.js", "config": { diff --git a/ui/app/src/edit.tag.pug b/ui/app/src/edit.tag.pug index d3064ab9f..e51630344 100644 --- a/ui/app/src/edit.tag.pug +++ b/ui/app/src/edit.tag.pug @@ -142,6 +142,7 @@ edit o.spec.enableReplicaConnectionPooler = i.spec.enableReplicaConnectionPooler || false o.spec.enableMasterPoolerLoadBalancer = i.spec.enableMasterPoolerLoadBalancer || false o.spec.enableReplicaPoolerLoadBalancer = i.spec.enableReplicaPoolerLoadBalancer || false + o.spec.maintenanceWindows = i.spec.maintenanceWindows || [] o.spec.volume = { size: i.spec.volume.size, diff --git a/ui/app/src/new.tag.pug b/ui/app/src/new.tag.pug index 9ae2f46da..0e687e929 100644 --- a/ui/app/src/new.tag.pug +++ b/ui/app/src/new.tag.pug @@ -594,6 +594,12 @@ new {{#if enableReplicaPoolerLoadBalancer}} enableReplicaPoolerLoadBalancer: true {{/if}} + {{#if maintenanceWindows}} + maintenanceWindows: + {{#each maintenanceWindows}} + - "{{ this }}" + {{/each}} + {{/if}} volume: size: "{{ volumeSize }}Gi"{{#if volumeStorageClass}} storageClass: "{{ volumeStorageClass }}"{{/if}}{{#if iops}} @@ -651,6 +657,7 @@ new enableReplicaConnectionPooler: this.enableReplicaConnectionPooler, enableMasterPoolerLoadBalancer: this.enableMasterPoolerLoadBalancer, enableReplicaPoolerLoadBalancer: this.enableReplicaPoolerLoadBalancer, + maintenanceWindows: this.maintenanceWindows, volumeSize: this.volumeSize, volumeStorageClass: this.volumeStorageClass, iops: this.iops, @@ -727,6 +734,10 @@ new this.enableReplicaPoolerLoadBalancer = !this.enableReplicaPoolerLoadBalancer } + this.maintenanceWindows = e => { + this.maintenanceWindows = e.target.value + } + this.volumeChange = e => { this.volumeSize = +e.target.value } @@ -1042,6 +1053,7 @@ new this.enableReplicaConnectionPooler = false this.enableMasterPoolerLoadBalancer = false this.enableReplicaPoolerLoadBalancer = false + this.maintenanceWindows = {} this.postgresqlVersion = this.postgresqlVersion = ( this.config.postgresql_versions[0] diff --git a/ui/manifests/deployment.yaml b/ui/manifests/deployment.yaml index f5ca17da9..76d2143cb 100644 --- a/ui/manifests/deployment.yaml +++ b/ui/manifests/deployment.yaml @@ -18,7 +18,7 @@ spec: serviceAccountName: postgres-operator-ui containers: - name: "service" - image: ghcr.io/zalando/postgres-operator-ui:v1.12.2 + image: ghcr.io/zalando/postgres-operator-ui:v1.13.0 ports: - containerPort: 8081 protocol: "TCP" diff --git a/ui/operator_ui/main.py b/ui/operator_ui/main.py index eb77418c8..ba544750f 100644 --- a/ui/operator_ui/main.py +++ b/ui/operator_ui/main.py @@ -465,6 +465,7 @@ def get_postgresqls(): 'status': status, 'num_elb': spec.get('enableMasterLoadBalancer', 0) + spec.get('enableReplicaLoadBalancer', 0) + \ spec.get('enableMasterPoolerLoadBalancer', 0) + spec.get('enableReplicaPoolerLoadBalancer', 0), + 'maintenance_windows': spec.get('maintenanceWindows', []), } for cluster in these( read_postgresqls( @@ -566,6 +567,11 @@ def update_postgresql(namespace: str, cluster: str): return fail('allowedSourceRanges invalid') spec['allowedSourceRanges'] = postgresql['spec']['allowedSourceRanges'] + if 'maintenanceWindows' in postgresql['spec']: + if not isinstance(postgresql['spec']['maintenanceWindows'], list): + return fail('maintenanceWindows invalid') + spec['maintenanceWindows'] = postgresql['spec']['maintenanceWindows'] + if 'numberOfInstances' in postgresql['spec']: if not isinstance(postgresql['spec']['numberOfInstances'], int): return fail('numberOfInstances invalid') diff --git a/ui/operator_ui/spiloutils.py b/ui/operator_ui/spiloutils.py index c2ac7118e..9de072fca 100644 --- a/ui/operator_ui/spiloutils.py +++ b/ui/operator_ui/spiloutils.py @@ -305,7 +305,7 @@ def read_versions( if uid == 'wal' or defaulting(lambda: UUID(uid)) ] -BACKUP_VERSION_PREFIXES = ['', '9.6/', '10/', '11/', '12/', '13/', '14/', '15/', '16/'] +BACKUP_VERSION_PREFIXES = ['', '10/', '11/', '12/', '13/', '14/', '15/', '16/'] def read_basebackups( pg_cluster,